2018年12月28日金曜日

ML Kit のサンプルコードと資料を更新しました。

8月1日に開催した ML for Mobile Developers の資料とハンズオンのコードを更新しました。

資料は12月14日の Google Developers ML Summit で発表した ML Kit の内容を利用したものに変更しました。




ハンズオンのコード https://github.com/yanzm/MLKitSample は最新の ML Kit SDK を使ったものに更新しました。

2018年12月17日月曜日

Kotlin メモ : require(), check()

require

指定した condition を満たさなかった場合 IllegalArgumentException が throw されます。IllegalArgumentException に渡す message を第2引数の lambda で指定することができます。 data class AccessKey(val value: String) { init { // value が empty の場合 IllegalArgumentException("value must not be empty") が throw される require(value.isNotEmpty()) { "value must not be empty" } } }


check

指定した condition を満たさなかった場合 IllegalStateException が throw されます。IllegalStateException に渡す message を第2引数の lambda で指定することができます。 data class AccessKey(val value: String) { init { // value が empty の場合 IllegalStateException("value must not be empty") が throw される check(value.isNotEmpty()) { "value must not be empty" } } }


2018年12月9日日曜日

sapporo.apk #2 で Jetpack について講演してきました。

sapporo.apk #2 で Jetpack の各機能の紹介をしてきました。おまけとして AndroidX の stable と最新のライブラリバージョンを載せてあります。

AndroidX は 1.0.0 がでて、そろそろ Support Library から AndroidX への移行を進めた方がよいでしょう。 次の 1.1.0-alpha もいくつかの artifact で出ています。 個人的には androidx.activity:activity:1.1.0-alpha02 で ViewModel を取得するための "by viewModels()" Kotlin property delegate が入ったのが熱いです。 class MainActivity : AppCompatActivity() { private val viewModel : MainViewModel by viewModels() ... }





2018年11月10日土曜日

sealed class に共通データを持たせるときは abstract val にする

sealed class Pet を継承した data class Cat と Dog があるとします。 sealed class Pet data class Cat(val name: String, val kind: CatKind) : Pet() data class Dog(val name: String, val kind: DogKind) : Pet() name プロパティは Pet として必須なので Pet class に持たせたいですね。
しかし次のように primary コンストラクタの property paramter として持たせようとするとコンパイルエラーになります。 sealed class Pet(val name: String) data class Cat(name: String, val kind: CatKind) : Pet(name) // コンパイルエラー data class Dog(name: String, val kind: DogKind) : Pet(name) // コンパイルエラー data class の primary コンストラクタは property parameter (val or var) しか持てないからです。 そこで、どうするかというと name を abstract val として Pet に持たせます。 sealed class Pet { abstract val name: String } data class Cat(override val name: String, val kind: CatKind) : Pet() data class Dog(override val name: String, val kind: DogKind) : Pet()


2018年11月4日日曜日

古い Mockito では Kotlin の suspend fun を override してくれないので 2.23.0 以降を使う

追記

mockito 2.23.0 で suspend fun のサポートが入った(Support mocking kotlin suspend functions compiled by Kotlin 1.3 (#1500))と教えていただいたので、試してみました。

2.23.0 の mockito なら以下のテストが成功しました!やったー! // このテストが 2.23.0 の mockito なら成功する! @Test fun test() { runBlocking { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } もともと試した mockito のバージョンは 2.8.9 です。(なぜこのバージョンかというと iosched がこのバージョンを使っていたからです。特に深い意味はありません。)


以下は mockito 2.8.9 での動作です。

以下のような interface と class があるとします。 interface Greeting { fun hello(): String } class GreetingTextCounter(private val greeting: Greeting) { fun count(): Int { return greeting.hello().length } } Greeting のモックを用意して特定の文字列を hello() で返すようにすれば、GreetingTextCounter.count() のテストができます。 @Test fun test() { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } ここで Greeting.hello() と GreetingTextCounter.count() を suspend にします。
interface Greeting { suspend fun hello(): String } class GreetingTextCounter(private val greeting: Greeting) { suspend fun count(): Int { return greeting.hello().length } } すると、以下のテストは失敗します。 // このテストは失敗する @Test fun test() { runBlocking { val mock = mock(Greeting::class.java).apply { `when`(hello()).thenReturn("Hello Android") } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } `when`().thenReturn() で hello() のときに "Hello Android" を返すように指定していても、GreetingTextCounter.count() のところで greeting.hello() が null を返してしまい、 java.lang.NullPointerException になります。

以下のように Greeting を実装した object を用意して hello() を override すればテストは成功します。 // このテストは成功する @Test fun test() { runBlocking { val mock = object : Greeting { override suspend fun hello() = "Hello Android" } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } しかし使用しないメソッド以外も override しなければいけないのでよくありません。

そこで Delegation を活用し、使用しないメソッドは Mockito の mock に流すようにします。 // このテストは成功する @Test fun test() { runBlocking { val mock = object : Greeting by mock(Greeting::class.java) { override suspend fun hello() = "Hello Android" } val counter = GreetingTextCounter(mock) assertThat(counter.count()).isEqualTo(13) } } これで必要なメソッドだけ override し、テストも通るようになります。


2018年10月26日金曜日

FlexboxLayoutManager では CompoundDrawable の指定に relative 系の属性、メソッドは使わないほうがよい

compileSdkVersion 28, 27 で試しています(将来のリリースで修正されている可能性があります)。

FlexboxLayoutManager の問題ではなく、TextView の measure() 実装の問題です(Issue Tracker に登録しました)。
どういう問題かというと、setCompoundDrawablesRelativeWithIntrinsicBounds()setCompoundDrawablesWithIntrinsicBounds() で TextView の measure() の結果が異なり、setCompoundDrawablesRelativeWithIntrinsicBounds() だと measuredWidth/Height に CompoundDrawables のサイズが含まれないという問題です。 再現コードなどは上記の Issue に書いてあります。

FlexboxLayoutManager は MeasuredWidth/Height の値を使って View を配置しているため、TextView で setCompoundDrawablesRelativeWithIntrinsicBounds() や android:drawableStart, android:drawableEnd などで CompoundDrawable を指定すると、それを含まないサイズで配置され、文字が切れたり折り返して表示されてしまいます。



以下は RecyclerView + FlexboxLayoutManager の例です。



この例では、2行目は android:drawableLeft, android:drawableRight、4行目は android:drawableStart, android:drawableEnd を使っています。それ以外は同じです。セットされているテキストも両方 "Hello Android" です。

2行目用のレイアウト <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableLeft="@drawable/square" android:drawableRight="@drawable/square" /> 4行目用のレイアウト <?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableEnd="@drawable/square" android:drawableStart="@drawable/square" /> android:drawableStart, android:drawableEnd を使った4行目では、CompoundDrawable のサイズが measuredWidth/Height に反映されず、その分文字の領域が小さくなって "Hello Android" の Android が切れたり、square の下のほうが切れたりしています。

このように CompoundDrawable を relative 系の属性、メソッドで指定すると問題があるため、setCompoundDrawablesWithIntrinsicBounds() や android:drawableLeft, android:drawableRight を使いましょう。



2018年9月15日土曜日

AlertDialog の Button の有効/無効を切り替える

AlertDialog の PositiveButton, NegativeButton, NeutralButton では、listener での実装によらずタップしたときに必ずダイアログが閉じます。

例えば AlertDialog でテキストを入力するようにして、未入力のときはボタンを押せないようにしたいとします。

AlertDialog の getButton() で Button インスタンスが取れるのでこれを利用します。
取得するボタンは BUTTON_POSITIVE, BUTTON_NEGATIVE, BUTTON_NEUTRAL で指定します。

あとは EditText に TextWatcher を追加して、テキストの変更時にボタンの isEnabled を変更します。

初回時 EditText が空ならボタンを disabled にしておかないといけません。
AlertDialog の getButton() は show() の前に呼ぶと NPE になるので注意が必要です。 val editText = EditText(this).apply { inputType = InputType.TYPE_CLASS_TEXT layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ).apply { val margin = (16 * resources.displayMetrics.density).toInt() marginStart = margin marginEnd = margin } } val frameLayout = FrameLayout(this).apply { addView(editText) } val dialog = AlertDialog.Builder(this) .setTitle("Title") .setMessage("Message") .setView(frameLayout) .setPositiveButton(android.R.string.ok, null) .create() editText.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable?) { } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = !s.isNullOrBlank() } }) dialog.show() dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false



Android Activity Transitions の xml 定義で exclude を指定する

コードでのやりかたは「Android Activity Transitions の対象から、Navigation Bar と Status Bar を外す(Activity Transitions を実装する その2)」に書きました。

xml で transition を定義する場合は以下のように targets タグと target タグを使います。 <?xml version="1.0" encoding="utf-8"?> <slide xmlns:android="http://schemas.android.com/apk/res/android" android:slideEdge="end"> <targets> <target android:excludeId="@android:id/navigationBarBackground" /> <target android:excludeId="@android:id/statusBarBackground" /> </targets> </slide> target タグには excludeId の他にも以下の属性を指定できます。
  • android:targetClass
  • android:targetId
  • android:excludeId
  • android:excludeClass
  • android:targetName
  • android:excludeName


2018年9月14日金曜日

android:windowCloseOnTouchOutside を指定するとどうなるのか

android:windowCloseOnTouchOutside は API Level 11 で追加されたテーマ用の属性で、true を指定すると、Dialog 系の theme を指定した Activity でダイアログ(というか window)以外の領域をタップしたときにダイアログが閉じます(というか Activity が finish() します)。

この属性は Window に関するもので、Window では以下のフィールドとメソッドが関連します。

Window public abstract class Window { ... private boolean mCloseOnTouchOutside = false; private boolean mSetCloseOnTouchOutside = false; ... /** @hide */ public void setCloseOnTouchOutside(boolean close) { mCloseOnTouchOutside = close; mSetCloseOnTouchOutside = true; } /** @hide */ public void setCloseOnTouchOutsideIfNotSet(boolean close) { if (!mSetCloseOnTouchOutside) { mCloseOnTouchOutside = close; mSetCloseOnTouchOutside = true; } } ... /** @hide */ public boolean shouldCloseOnTouch(Context context, MotionEvent event) { final boolean isOutside = event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) || event.getAction() == MotionEvent.ACTION_OUTSIDE; if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) { return true; } return false; } private boolean isOutOfBounds(Context context, MotionEvent event) { final int x = (int) event.getX(); final int y = (int) event.getY(); final int slop = ViewConfiguration.get(context).getScaledWindowTouchSlop(); final View decorView = getDecorView(); return (x < -slop) || (y < -slop) || (x > (decorView.getWidth()+slop)) || (y > (decorView.getHeight()+slop)); } } Window の shouldCloseOnTouch() では、mCloseOnTouchOutside が true、かつ peekDecorView が null ではない、かつ MotionEvent の action が MotionEvent.ACTION_OUTSIDE、もしくは MotionEvent.ACTION_DOWN でタップ位置が DecorView の外側の場合、true が返ります。

Window の setCloseOnTouchOutside() は hide になっていて通常のアプリからは呼べません。 ではコードでは指定できないのかというと、Activity の setFinishOnTouchOutside() から指定できます。

Activity public void setFinishOnTouchOutside(boolean finish) { mWindow.setCloseOnTouchOutside(finish); } Activity の onTouchEvent() で Window の shouldCloseOnTouch() を呼んでおり、これにより DecorView の外側をタップすると Activity が finish() します。

Activity public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }

ちなみに android:windowCloseOnTouchOutside 属性の設定値は、PhoneWindow から Window.setCloseOnTouchOutsideIfNotSet() を呼ぶことで適用されています。



2018年9月7日金曜日

Android Activity Transitions の対象をグループ化する

ActivityTransition に Slide を指定すると、デフォルトでは View ごとに別々にアニメーションします。

例えばボタンを縦に並べた Activity に Slide で enter すると、スライド中はボタンの間隔が広がって、徐々に詰まっていきます。

左 → 右 : スライド中


ボタン同士の配置そのままに Slide させるには、ボタンの親の ViewGroup の setTransitionGroup() で true をセットします。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) button.setOnClickListener { startActivity( Intent(this, MainActivity2::class.java), ActivityOptions.makeSceneTransitionAnimation(this).toBundle() ) } } } class MainActivity2 : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.requestFeature(Window.FEATURE_CONTENT_TRANSITIONS) window.enterTransition = Slide() window.exitTransition = Slide() setContentView(R.layout.activity_main2) // これを追加 constraintLayout.isTransitionGroup = true } }

左 → 右 : スライド中



解説

ViewGroup の captureTransitioningViews() で isTransitionGroup() が false の場合、各子 View の captureTransitioningViews() を呼びます。 View の captureTransitioningViews() では VISIBLE な場合自身を transition するリストに追加します。

つまり、ViewGroup の setTransitionGroup() で true をセットすると、その ViewGroup が transition するリストに追加され、子 View はされなくなります。 public abstract class ViewGroup ... { ... /** @hide */ @Override public void captureTransitioningViews(List<View> transitioningViews) { if (getVisibility() != View.VISIBLE) { return; } if (isTransitionGroup()) { transitioningViews.add(this); } else { int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); child.captureTransitioningViews(transitioningViews); } } } ... } public class View ...{ ... public void captureTransitioningViews(List<View> transitioningViews) { if (getVisibility() == View.VISIBLE) { transitioningViews.add(this); } } ... }


2018年8月6日月曜日

Kotlin メモ : padStart, padEnd

padStart(length: Int, padChar: Char = ' ')
padEnd(length: Int, padChar: Char = ' ')

CharSequence と String の拡張関数として用意されています。

length で指定した長さに足りない場合、padChar で start/end を埋めたものを返します。
padChar を省略した場合、' ' が使われます。

assertThat("a".padStart(3)).isEqualTo(" a") assertThat("aa".padStart(3)).isEqualTo(" aa") assertThat("aaa".padStart(3)).isEqualTo("aaa") assertThat("aaaa".padStart(3)).isEqualTo("aaaa") assertThat("a".padStart(3, '-')).isEqualTo("--a") assertThat("aa".padStart(3, '-')).isEqualTo("-aa") assertThat("aaa".padStart(3, '-')).isEqualTo("aaa") assertThat("aaaa".padStart(3, '-')).isEqualTo("aaaa") assertThat("a".padEnd(3)).isEqualTo("a ") assertThat("aa".padEnd(3)).isEqualTo("aa ") assertThat("aaa".padEnd(3)).isEqualTo("aaa") assertThat("aaaa".padEnd(3)).isEqualTo("aaaa") assertThat("a".padEnd(3, '-')).isEqualTo("a--") assertThat("aa".padEnd(3, '-')).isEqualTo("aa-") assertThat("aaa".padEnd(3, '-')).isEqualTo("aaa") assertThat("aaaa".padEnd(3, '-')).isEqualTo("aaaa")

2018年8月1日水曜日

Android で Dagger を使う(その4 : @BindsInstance)

Android では @Provides の引数として Application が必要だったり、@Inject で Application を渡したいことがあります。

Module のコンストラクタで Application のインスタンスを渡すようにすることで実現できますが、もっといい方法があります。

まず Component に @Component.Builder をつけた Builder インターフェースを用意します。
インタフェースの中に @BindsInstance アノテーションをつけたメソッドを用意し、Application インスタンスを渡せるようにします。 @Component( modules = [ AppModule::class ] ) internal interface AppComponent { @Component.Builder interface Builder { fun build(): AppComponent @BindsInstance fun application(application: Application): Builder } ... } あとは Component を構成するときに、用意したメソッドで Application インスタンスを渡すだけです。 class MyApplication : Application() { override fun onCreate() { super.onCreate() val appComponent = DaggerAppComponent .builder() .application(this) .build() ... } } 生成されたコードでは DaggerAppComponent が application インスタンスを保持し、MembersInjector や Factory に適宜渡しています。


2018年6月20日水曜日

LiveData を UnitTest でテストする

デザートの文字列を保持して、追加・削除されたタイミングで保持数を LiveData で通知する DessertsHolder を UnitTest でテストしてみましょう。 class DessertsHolder { private val counter = MutableLiveData<Int>() private val list = mutableListOf<String>() init { counter.value = 0 } fun getCounter(): LiveData<Int> = counter fun add(item: String) { list.add(item) counter.value = list.size } fun remove(item: String) { list.remove(item) counter.value = list.size } fun clear() { list.clear() counter.value = 0 } } 特に何もせず次のような普通の UnitTest を書くと、Looper が mock されていないというエラー(RuntimeException: Method getMainLooper in android.os.Looper not mocked. )がでます。 class DessertsHolderTest { @Test fun test() { val holder = DessertsHolder() holder.add("Donuts") assertThat(holder.getCounter().value).isEqualTo(1) } }

そこで、まず AAC の core-testing ライブラリを追加します。 dependencies { def lifecycle_version = "1.1.1" testImplementation "android.arch.core:core-testing:$lifecycle_version" } AndroidX dependencies { def lifecycle_version = "2.0.0" testImplementation "androidx.arch.core:core-testing:$lifecycle_version" }
そして @get:Rule で rule に InstantTaskExecutorRule を指定します。このとき get: をつけないと rule が public ではないという ValidationError (ValidationError: The @Rule 'rule' must be public.)が起こるので注意しましょう。 class DessertsHolderTest { @get:Rule val rule: TestRule = InstantTaskExecutorRule() @Test fun test() { ... } }


2018年6月9日土曜日

ViewOutlineProvider を使う

API Level 21 に追加された ViewOutlineProvider では shadow casting と outline clipping に利用する Outline を指定できます。

ViewOutlineProvider の getOutline() の引数で渡される Outline に Rect, RoundRect, Oval または ConvexPath を指定します。

View が持つ Drawable が invalidate されたり、View のサイズが変わったり、View の invalidateOutline() が呼ばれると getOutline() が呼ばれます。 private val clipOutlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { val margin = min(view.width, view.height) / 10 outline.setOval( 0, 0, view.width, view.height ) } } View の setOutlineProvider() で ViewOutlineProvider を指定します。outline clipping を有効にするには setClipToOutline() で true を指定します。

outline clipping は現状 RoundRect か Circle(Oval で縦横のサイズが同じ)のときだけ効きます。
clip しても View のサイズには影響しないためクリック領域などはそのままです。 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) clippedView.outlineProvider = clipOutlineProvider clippedView.clipToOutline = true } 左 : clipToOutline = false, 右 : clipToOutline = true





2018年6月7日木曜日

ConstraintLayout で float で指定する属性

ConstraintLayout 1.1.0 の属性のうち、TypedArray から getFloat() で取得されている属性は
  • app:layout_constraintCircleAngle
  • app:layout_constraintGuide_percent
  • app:layout_constraintHorizontal_bias
  • app:layout_constraintVertical_bias
  • app:layout_constraintHeight_percent
  • app:layout_constraintWidth_percent
  • app:layout_constraintHorizontal_weight
  • app:layout_constraintVertical_weight
です。

これらの属性は app:layout_constraintGuide_percent="0.3" のように値を直接指定するときは float 形式で書きます。

float 値を resource で定義するには、 <?xml version="1.0" encoding="utf-8"?> <resources> <item name="guideline_percent" type="dimen" format="float">0.3</item> </resources> のように item タグ を使って float format、dimen type で定義します。 app:layout_constraintGuide_percent="@dimen/guideline_percent"


2018年5月31日木曜日

I/O Recap : Slices

https://developer.android.com/guide/slices/

Slices は自分のアプリの情報を他のアプリに表示するための仕組みです。
コンセプトとしては App Widget で使う RemoteViews と似ていますが、使えるレイアウトは用意されているテンプレートだけなど制限があります。 Google Search の検索結果で利用することを例示されていることからも、統一された Look and Feel を持たせたいのかなと思います。

Google Search アプリで2018年夏に対応することが Google I/O 2018 のセッションでアナウンスされていました。 将来的に Google Assistant での応答結果に利用できるようになることもアナウンスされていました。

Slices のサポートが Jetpack に組み込まれており、これを利用すると Android 4.4 以降に対応できます。

前提条件

  • AndroidX にリファクタリングされていること
  • 必須ではないが Android Studio 3.2 以降には Slice を開発するのに便利なツールや機能が追加されている

Slice Provider Widzard

Android Studio 3.2 には Slice Provider を追加する Widzard が用意されています。 この Widzard を使うと
  • build.gradle に slice の dependencies を追加
  • SliceProvider を継承したクラスの生成
  • AndroidManifest に Provider の宣言を追加
を自動で行ってくれます。

* Android Studio 3.2 Canary 15 まで Wizard で追加される dependencies が typo しているバグがありました。Canary 16 では修正されています。 https://issuetracker.google.com/issues/79996770


dependencies の設定

現時点(2018年5月31日)での最新版は 1.0.0-alpha2 です。 implementation "androidx.slice:slice-core:1.0.0-alpha2" implementation "androidx.slice:slice-builders:1.0.0-alpha2"

SliceProvider を継承したクラスを用意

自分のアプリで Slice を提供するには SliceProvider を継承したクラスを用意します。 androidx.slice.SliceProviderandroid.app.slice.SliceProvider があるので注意してください。 import androidx.slice.Slice import androidx.slice.SliceProvider ... class MySliceProvider : SliceProvider() { override fun onCreateSliceProvider(): Boolean { return true } override fun onBindSlice(sliceUri: Uri): Slice? { ... } }

AndroidManifest に Provider の宣言を追加

android:exported="true" が必要です。 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.example.slicecodelab"> <application ... > ... <provider android:name=".MySliceProvider" android:authorities="com.android.example.slicecodelab" android:exported="true" /> </application> </manifest>

Uri と Slice の bind

各 Slice は URI と紐づいており、Slice の表示がリクエストされると SliceProvider の onBindSlice(uri: Uri) が呼ばれます。
onBindSlice(uri: Uri) では送られてきた Uri を処理し、対応する Slice インスタンスを返します。 override fun onBindSlice(sliceUri: Uri): Slice? { return when (sliceUri.path) { "/temperature" -> { ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("Temperature : $temperature") } .build() } else -> null } } 引数の sliceUri には content://<authorities>/<path> が入ってきます。


Slice の更新

Slice で表示している内容を更新したいときは Slice の Uri に対して notifyChange() を呼びます。 そうすると Provider の onBindSlice(uri: Uri) が呼ばれるので新しい内容で Slice を作ります。 class MyBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // update slice related data ... context.contentResolver.notifyChange(sliceUri, null) } }

Templates

https://developer.android.com/guide/slices/templates

Slice は TemplateSliceBuilder を 継承したクラスを使って作ります。 このうち、一番外側の Slice は ListBuilder で作ります。


ListBuilder

List 形式の Slice を作成できます。
子要素(一行の要素)として ListBuilder.RowBuilder、ListBuilder.HeaderBuilder、ListBuilder.RangeBuilder、ListBuilder.InputRangeBuilder、GridRowBuilder を追加できます。HeaderBuilder は一つしか指定できませんが、その他は複数追加できます。

MODE_SHORTCUT では、Header に PrimaryAction が指定されていればそれが使われ、なければ最初の Row の PrimaryAction が使われます。
MODE_SMALL では 1 row だけ表示されます。Header が指定されていればそれだけが表示され、なければ最初の Row だけが表示されます。
MODE_LARGE では表示可能な数だけ row が表示されます。Slice を表示する側が scrolling に対応していれば全ての row がスクロールできるビュー内に表示されます。

ListBuilder のコンストラクタでは TTL をミリ秒で指定します。time sensitive なコンテンツを含まない場合は ListBuilder.INFINITY を指定します。

setAccentColor(color: Int) で accentColor を指定できます。accentColor はアイコンの tint や toggle の tint、horizontal progress の tint などに適用されます。 private fun createSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setAccentColor(Color.BLUE) ... .build() }


setKeywords(keywords: List) で指定したキーワードは Slice の Hints(getHints() で取得できる)に追加されます。 private fun createSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setKeywords(listOf("Cupcake", "Donuts", "Eclair")) ... .build() }

WiFi 設定など表示したい row が多いときは setSeeMoreAction(intent: PendingIntent) を指定しておくと、全ての row が表示できないときに右下に See more ボタンが表示されるようになります。 private fun createSlice(sliceUri: Uri): Slice? { val seeMoreIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setSeeMoreAction(seeMoreIntent) ... .build() }


addAction() で Header の右側に SliceAction を表示させることができます(Header がない場合最初の Row の右側に表示されますが、Header が指定されている前提の機能だと思います)。
複数追加できますが 3つまでしか表示されませんでした。 private fun createSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val homeAction = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE, "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addAction(homeAction) .setHeader { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") } .build() }


ListBuilder.RowBuilder

ListBuilder の子要素(一行の要素)として Row を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、それだけではリストに追加されず ListBuilder.addRow() で明示的にリストに追加する必要があります。

setPrimaryAction() で指定した SliceAction は Row がタップされた時だけでなく、モードが SliceView.MODE_SHORTCUT のときにも使われます。

title, subTitle の第2引数で load 中かどうかを指定すると、SliceItem の Hints(getHints() で取得できる)に Slice.HINT_PARTIAL が追加されます。 private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val action = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_launcher), "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setPrimaryAction(action) } .build() } 左 : MODE_SHORTCUT、右 : MODE_LARGE




addEndItem(timeStamp: Long) で右側に timestamp を表示することができます。
指定した timeStamp の long 値から自動で差分時間表示(10 min ago とか)になります。 private fun createRowSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(System.currentTimeMillis() - 10 * 60 * 1000) } .build() }


addEndItem(icon: IconCompat, imageMode: Int) で右端にアイコンを表示することができます。

imageMode には のどれかを指定します。 ICON_IMAGE を指定すると小さいサイズで表示され tint されます。SMALL_IMAGE、LARGE_IMAGE では tint されません。

アイコンは複数追加できますが 3つまでしか表示されませんでした。 private fun createRowSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem( IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE) } .build() } 左 : ICON_IMAGE、中 : SMALL_IMAGE、右 : LARGE_IMAGE



左 : ICON_IMAGE での MODE_SHORTCUT、右 : LARGE_IMAGE での MODE_SHORTCUT




addEndItem(action: SliceAction) で右側に SliceAction を表示できます。

EndItem にアイコン(上記)と SliceAction を併用することはできません。
private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val homeAction = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.ICON_IMAGE, "Home") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(homeAction) } .build() }


toggle 表示用の SliceAction を指定するとスイッチを表示できます。 private fun createRowSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val toggleAction = SliceAction( pendingIntent, "Toggle", true) return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRow { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .addEndItem(toggleAction) } .build() }


setTitleItem() で左側に Timestamp、アイコン、SliceAction が表示できるはずなのですが、指定しても表示されませんでした。


ListBuilder.HeaderBuilder

ListBuilder の子要素(一行の要素)として Header を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、それだけではリストにセットされず ListBuilder.setHeader() で明示的にリストにセットする必要があります。
ListBuilder にセットできる HeaderBuilder は一つだけです。

title, subTitle に加えて summary をセットすることができ、summary はモードが SliceView.MODE_SMALL のときに使われます。

setPrimaryAction() で指定した SliceAction は Header がタップされた時だけでなく、モードが SliceView.MODE_SHORTCUT のときにも使われます。

title, subTitle, summary の第2引数で load 中かどうかを指定すると、SliceItem の Hints(getHints() で取得できる)に Slice.HINT_PARTIAL が追加されます。 private fun createHeaderSlice(sliceUri: Uri): Slice? { val pendingIntent = ... val action = SliceAction( pendingIntent, IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground), "Temperature controls") return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .setHeader { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setSummary("summary") .setPrimaryAction(action) } .build() } 左: MODE_SHORTCUT、中 : MODE_SMALL、右 : MODE_LARGE




ListBuilder.RangeBuilder

ListBuilder の子要素(一行の要素)として horizontal progress を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addRange() で明示的にリストに追加する必要があります。

Min は 0、Max のデフォルト値は 100、Value のデフォルト値は 0 です。 private fun createRangeSlice(sliceUri: Uri): Slice? { return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addRange { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setMax(10) .setValue(5) } .build() } 左: setMax(), setValue() のみ、右 : title, subTitle あり




ListBuilder.InputRangeBuilder

ListBuilder の子要素(一行の要素)として horizontal slider を表示できます。
コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addInputRange() で明示的にリストに追加する必要があります。

Min のデフォルト値は 0、Max のデフォルト値は 100、Value のデフォルト値は 0 です。
InputAction は必須です。 private fun createInputRangeSlice(sliceUri: Uri): Slice? { val pendingIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addInputRange { it.setTitle("title") .setSubtitle("sub title") .setContentDescription("description") .setMin(5) .setMax(10) .setValue(8) .setThumb(IconCompat.createWithResource(context, R.drawable.square)) .setInputAction(pendingIntent) } .build() } 左: setMin(), setMax(), setValue() のみ、中 : title, subTitle あり、右 : setThumb()




GridRowBuilder

ListBuilder の子要素(一行の要素)として horizontal grid を表示できます。
grid の各アイテムは GridRowBuilder.CellBuilder で作成します。

コンストラクタで親の ListBuilder インスタンスを指定する必要がありますが、 それだけではリストに追加されず ListBuilder.addGridRow() で明示的にリストに追加する必要があります。

ListBuilder と同じように setSeeMoreAction(intent: PendingIntent) で see more ボタンを表示する機能があります。


GridRowBuilder.CellBuilder

GridRowBuilder の子要素(1アイテム)として cell を表示できます。

コンストラクタで親の GridRowBuilder インスタンスを指定する必要がありますが、 それだけではグリッドに追加されず GridRowBuilder.addCell() で明示的にグリッドに追加する必要があります。

addTitleText() でタイトルスタイルで表示したいテキストを追加し、addText() でノーマルスタイルで表示したいテキストを追加します。 両方含め最初に追加したテキスト2つが利用され、それ以降に追加されたテキストは無視されます。

addImage() で画像を追加します。最初に追加した画像1つが利用され、それ以降に追加した画像は無視されます。

setContentIntent() で cell をタップしたときに発行される PendingIntent を指定することができます。 private fun createGridSlice(sliceUri: Uri): Slice? { ... val action = ... val seeMoreIntent = ... return ListBuilder(context, sliceUri, ListBuilder.INFINITY) .addGridRow { it.setContentDescription("description") .setPrimaryAction(action) .setSeeMoreAction(seeMoreIntent) .addCell { it.addImage( IconCompat.createWithResource(context, R.drawable.ic_home), ListBuilder.LARGE_IMAGE) .addTitleText("Title") .addText("normal") .setContentDescription("description") .setContentIntent(cellIntent) } ... } .build() }

ドキュメントには cell の数について言及されていませんが、SliceViewer(後述)では5つまで表示されました。
また、setSeeMoreAction() すると、4つ表示 + More になりました。



なぜか SliceViewer(1.0.0-alpha1.1 で確認)では画像が長方形に表示されてしまうのですが、Google Search が対応したときにはちゃんと正方形で表示されると思います。

画像のみ



左: テキストのみ MODE_SMALL、右: テキストのみ MODE_LARGE



左: 画像 + テキスト MODE_SMALL、右: 画像 + テキスト MODE_LARGE



MODE_SMALL では表示されるテキストが1つだけになります。



Slice Viewer

https://developer.android.com/guide/slices/getting-started#run-the-slice-viewer

自分のアプリの Slice の表示を確認するために SliceViewer サンプル が用意されています。
このサンプルの apk ダウンロードしデバイスにインストールします。 $ adb install -r -t slice-viewer.apk 自分の Slice を表示確認をするには、slice-<表示させたい Slice の URI> を Intent の Data として指定します。 $ adb shell am start -a android.intent.action.VIEW -d slice-content://com.android.example.slicecodelab/temperature ランチャーから SliceViewer アプリを起動し、アプリ内の検索バーで <表示させたい Slice の URI> を検索すると、トップの一覧画面に検索した Slice が表示されます。


Codelabs

Creating Android Slices
  • Java のみで Kotlin 版はない
  • androidx.slice のバージョンが 1.0.0-alpha1 になっているが 1.0.0-alpha2 が出ている
  • P と androidx で同じ名前のクラスでも API が異なるので import 先が androidx になっているか注意すべし

Google I/O 2018 session





2018年5月26日土曜日

I/O Recap : ML Kit 情報まとめ(Android 向け)

* 以下は 2018年5月25日時点での情報です。


ML Kit for Firebase

現在 ML Kit はベータで、以下の機能を Android と iOS で利用することができます。
  • テキスト認識(Text recognition)
  • 顔検出(Face detection)
  • バーコードの読み取り(Barcode scanning)
  • 画像のラベルづけ(Image labeling)
  • ランドマーク認識(Landmark recognition)
  • 独自 TensorFlow Lite モデルの実行(TensorFlow Lite model serving)
  • (Google I/O 2018 のセッションで High density face contour feature と Smart Replay API が Coming soon であると紹介されています。いずれも on-device で real time に動作するようです)

これらの処理にはデバイス上で行う on-device API とクラウドで実行される cloud-based API が用意されています。
on-device API はリアルタイムに処理でき、オフラインでも動作し、無料で使うことができます。
cloud-based API(Cloud Vision API)は on-device API よりも詳しい情報を提供しますが、一定の利用回数以上は有料です。

例えばテキスト認識では、on-device API では Latin-based language しか認識できず、他の言語も認識したいなら cloud-based API を使う必要があります。
画像のラベルづけでは、on-device API では 400+ labels ですが cloud-based API なら 1000+ labels に対応しています。

Cloud Vision API を利用するには Firebase の課金プランを Blaze(従量制課金)にする必要があります。
Firebase Pricing Plans

機能ごとに毎月1000 API calls までは無料で使うことができます。
Cloud Vision API Pricing

独自の TensorFlow Lite モデルを Firebase に upload するだけで、アプリからそのモデルを実行できるようになります。 モデルのホスティングと実行は無料で使うことができます。


API には on-device でのみ使えるもの、Cloud でのみ使えるもの、両方用意されているものがあります。

機能on-deviceCloud
テキスト認識oo
顔検出o
バーコード読み取りo
画像のラベルづけoo
ランドマーク認識o
独自モデルの実行o


テキスト認識

https://firebase.google.com/docs/ml-kit/recognize-text

画像からテキストを認識します。

on-device API と cloud-based API 両方用意されています。 on-device API は Latin-based language のみ認識でき、cloud-based API は他の言語にも対応しています。

on-device API は無料で使うことができ、cloud-based API は毎月1000 API calls までは無料で使うことができます。
cloud-based API を使うには Firebase の課金プランを Blaze(従量制課金)にする必要があります。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }
cloud-based API には通常の文字認識用の FirebaseVisionCloudTextDetector の他に、書類のように文字密度の高いテキストの認識用に FirebaseVisionCloudDocumentTextDetector が用意されています。


顔検出

https://firebase.google.com/docs/ml-kit/detect-faces

画像から顔を検出します。
目・耳・頬・鼻・口の位置を取得できます。
笑顔かどうか(笑顔の確率)を取得できます。
目が閉じているかどうか(目が閉じている確率)を取得できます。
検出された個々の顔ごとの識別子を取得でき、動画のフレーム間で同一の顔をトラッキングできます。

on-device API のみです。無料で使うことができます。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }

バーコード読み取り

https://firebase.google.com/docs/ml-kit/read-barcodes

画像からバーコードを読み取ります。

対応フォーマット
  • Linear formats: Codabar, Code 39, Code 93, Code 128, EAN-8, EAN-13, ITF, UPC-A, UPC-E
  • 2D formats: Aztec, Data Matrix, PDF417, QR Code
バーコードの向きに関係なく認識します。

on-device API のみです。無料で使うことができます。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }

画像のラベルづけ

https://firebase.google.com/docs/ml-kit/label-images

追加のメタ情報なしで画像内のエンティティ(人、物、場所、活動など)を認識し、リストとして取得できます。

on-device API と cloud-based API 両方用意されており、on-device API は 400+ labels に、cloud-based API は 1000+ labels に対応しています。
on-device API は無料で使うことができ、cloud-based API は毎月1000 API calls までは無料で使うことができます。
cloud-based API を使うには Firebase の課金プランを Blaze(従量制課金)にする必要があります。

最新は 16.0.0、on-device API は 15.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' // on-device implementation 'com.google.firebase:firebase-ml-vision-image-label-model:15.0.0' }

ランドマーク認識

https://firebase.google.com/docs/ml-kit/recognize-landmarks

画像からランドマーク(例えば東京タワーなど)を認識します。

cloud-based API のみです。そのため Firebase の課金プランを Blaze(従量制課金)にする必要があります。毎月1000 API calls までは無料です。

最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-vision:16.0.0' }

独自 TensorFlow Lite モデルの実行

https://firebase.google.com/docs/ml-kit/use-custom-models

独自の TensorFlow Lite モデルは Firebase console からアップロードします。

アプリへのモデルのダウンロードは Firebase が動的に行ってくれるため、APK にモデルをバンドルする必要がありません。これによりアプリのインストール時のサイズを減らすことができます。
また、アプリのリリースとモデルのリリース(Firebase への upload)プロセスが分離されることで、それぞれのチームでリリースをハンドリングできるようになります。

Firebase Remote Config と組み合わせれば A/B test を行うこともできます。
full TensorFlow モデルを lightweight TensorFlow Lite モデルへ変換・圧縮する機能が coming soon だと I/O で発表されています。

(Firebase console に upload せずに)APK にモデルをバンドルしたり、自分のサーバーでモデルをホストしてアプリにダウンロードして、それを ML Kit の API 経由で使うこともできます。


最新は 16.0.0 です。 dependencies { implementation 'com.google.firebase:firebase-ml-model-interpreter:16.0.0' } upload した独自モデルを使うには、FirebaseCloudModelSource.Builder にモデル名を渡して指定します。
(I/O のセッション動画のコードが古いので注意)
モデル名は Firebase にモデルをアップロードするときに指定します。あとから変更はできません。 val cloudSource = FirebaseCloudModelSource.Builder("my_model_v1") ... .build() RemoteConfig でモデル名を切り替えるようにすれば、target ごとにそれぞれ異なるモデルを使うことができます。 val modelName = firebaseRemoteConfig.getString("my_model") val cloudSource = FirebaseCloudModelSource.Builder(modelName) ... .build()

High density face contour feature

I/O のセッション動画 より



100以上の点を検出し 60fps で処理できるとのこと。 coming pretty soon だそうです。


ML Kit console

左側のメニューの [DEVELOP] - [ML Kit] で ML Kit のコンソールを開くことができます。



ここのカスタムタブから独自モデルをアップロードします。



Codelabs

最初に Codelabs のアプリで動作を見てからドキュメントを読むのがよいと思います。
実際に両方ともやりましたが on-device での認識が速くすごいと思いました。

Android 向けの ML Kit のコードラボは2つ用意されています。
サンプルコードは Java で Kotlin 版は用意されていません。

Recognize text in images with ML Kit for Firebase
  • アプリにあらかじめ用意されている画像からテキストを認識する
  • on-device API と cloud-based API 両方使う
  • Cloud Vision API (https://console.cloud.google.com/apis/library/vision.googleapis.com/)を試すには Firebase の料金プランを Blaze(従量課金制)にしないといけない
左: on-device API での認識結果、右 : cloud-based API での認識結果




Identify objects in images using custom machine learning models with ML Kit for Firebase
  • アプリにあらかじめ用意されている画像に対し独自 TensowFlow Lite モデルを実行し、物体を認識する
  • TensorFlow Lite のモデルファイルが用意されているので Firebase console の設定等実際に試すことができてよい



Quick Start Sample

コードラボはテキスト認識と独自モデルしかないので、その他の機能を使い方をみるには Github で公開されている Quick Start Sample が参考になります。
  • Live Preview : カメラのプレビューに対して on-device API (顔検出、テキスト認識、バーコード読み取り、画像のラベルづけ、独自モデル)を実行
  • Still Image : 画像に対して cloud-based API(画像のラベルづけ、ランドマーク認識、テキスト認識)を実行





Google I/O 2018 session



Reference




2018年5月18日金曜日

IO recap : Android vitals: debug app performance and reap rewards (Google I/O '18)



星1つのレビューでは42%のユーザーが安定性やバグについて言及している
星5つのレビューでは73%のユーザーがスピード、デザイン、使いやすさについて言及している

ANR 率やクラッシュ率が上がると、ユーザーがアプリで費やす時間が有意に減る


Android vitals とは、Android デバイスの安定性とパフォーマンスを向上するための Google が主導する取り組み
もっとも重要なパフォーマンスメトリクスであるバッテリー、安定性、レンダリングの情報を開発者にわかりやすく提供する
データ提供を opt in している1億以上のユーザーからの情報

昨年 Android vitals をリリースしてから 10,000 以上の開発者がコンソールからパフォーマンスを理解した
昨年に比べ、スピード、デザイン、使いやすさについて言及している星5つのレビューは 4% 増え、安定性やバグに言及している星1つのレビューは 18% 減り、リソースの使用について言及している星1つのレビューは 21% 減った


Starbucks アプリは ANR rate が 70% 減り、Crash rate が 85% 減った
ANR は 3rd party のライブラリで起こっていたため、自分たちの観測に引っかかっていなかった
Android vitals はプラットフォームレベルのツールなので、Starbucks の 3rd party crash SDK では検出できていなかったクラッシュを見つけることができた
なぜならそのクラッシュは 3rd party crash SDK が開始される前に起こっていたため

Kiloo の Subway Surfers というゲームでは ANR を 95% 減らすことができた


昨年 Android vitals を公開したとき、バッテリー、安定性、レンダリングの3つの項目があった
新しくアプリのスタートアップ時間(App startup time)と権限(Permissions)が追加された

Vitals
  • バッテリー(Battery)
  • 安定性(Stability)
  • レンダリング(Rendering)
  • New: アプリのスタートアップ時間(App startup time)
    • コールド スタートアップ時間が長い(Slow cold start)
    • ウォーム スタートアップ時間が長い(Slow warm start)
    • ホット スタートアップ時間が長い(Slow hot start)
  • New: 権限(Permissions)
    • 権限リクエストの拒否率(Permission request denials)

バッテリー(Battery)

  • 停止した wake lock
  • 過度の wakeup
  • 過度のバックグラウンドでの Wi-Fi スキャン
  • 過度のバックグラウンドでのネットワーク使用

安定性(Stability)

  • ANR 発生率
  • クラッシュ発生率

レンダリング(Rendering)

  • フリーズした UI フレーム
  • 遅いレンダリング

New: アプリのスタートアップ時間(App startup time)

  • コールド スタートアップ時間が長い : 5秒以上
  • ウォーム スタートアップ時間が長い : 2秒以上
  • ホット スタートアップ時間が長い : 1.5秒以上

New Metric: コールド スタートアップ時間が長い
  • 5秒以上かかると遅いと判断
  • コールドスタート :
    • Activity が起動してから running になるまで
    • Activity launched → onCreate() → onStart() → onResume() → Activity running
    • アプリがしばらく使われておらず、アプリがメモリ上にいない状態からスタート

New Metric: ウォーム スタートアップ時間が長い
  • 2秒以上かかると遅いと判断
  • ウォームスタート :
    • Activity が起動してから running になるまで
    • Activity launched → onCreate() → onStart() → onResume() → Activity running
    • アプリが最近使われており、アプリがメモリ上にいる状態からスタート(アプリはkillされていない)

New Metric: ホット スタートアップ時間が長い
  • 1.5秒以上かかると遅いと判断
  • ホットスタート :
    • onRestart() から running になるまで
    • onRestart() → onStart() → onResume() → Activity running
    • アプリと Activity がメモリ上にいる状態からスタート

New: 権限(Permissions)

アプリのコアバリューに必要な権限だけをリクエストし、必要に応じて権限リクエストの正当な理由をランタイム時に提供する
  • 権限リクエストの拒否率
〜40%のユーザーが権限を拒否した理由として、その権限は不必要だと思ったと回答している

権限の詳細ビューでは権限をグループに分けて表示しているので、どの権限がユーザーにとって納得感があり、どの権限が不必要だと思われているかがわかる

Android Vitals の詳細の内訳

  • 一般的な内訳
    • APK versionごと
    • デバイスごと
    • Android versionごと
  • Wake locks, wakeups
    • tag ごと
  • ANR率
    • Activity 名ごと
    • ANR type ごと
    • Clusters
  • クラッシュ率
    • Clusters

カテゴリーベンチマーク

自分のアプリの vital が特定のカテゴリーの中でどのくらい良いかを見ることができる
vital の各 metric でパーセンタイル 25, 50, 75 の値を見ることができる

概要画面で全ての vital がリストされ、直近の30日とその前の30日の値、ベンチマークの値を見ることができる



概要画面の主な指標(Core Vitals)には Google Play でのアプリの表示やランキングに影響するパフォーマンス指標が表示される
主な指標が下位25%より悪くなると Bad behavior として表示される

異常検知(Anomaly Detection)

新リリースやリグレッションの結果値に急変があるとアラートを出す
  • ANRやクラッシュ率の大きな変化
  • 主な指標(Core Vitals)の大きな変化
概要画面右上の[通知設定]からAlertをメールで受け取るよう設定できる




主な指標(Core Vitals)を改善するには

ANRの原因
  • Network / Disk operations
  • Long calculations
  • InterProcess Communication (IPC)
  • Locks and Synchronization
  • Slow BroadcastReceiver handling

Network / Disk operations

例: SharedPreferences インスタンスを生成する時点で Disk 処理が行われる override fun onCreate(state: Bundle?) { // この時点で Disk 処理が行われる prefs = PreferenceManager.getDefaultSharedPreferences(this) } どのメソッドが Network 処理や Disk 処理をするのか理解するのは難しいので StrictMode を利用する class MyApplication : Application() { override fun onCreate() { super.onCreate() if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .penaltyDeath() .build()) } } }

Long calculations

例: 数独ゲームの盤面生成に時間がかかる

Android Profiler で CPU の使用をチェックする
ちなみに Android Studio 3.2 Canary ではスタートアップ時間をプロファイルできるようになっている

StrictMode にはこのメソッドを呼ぶと遅くなるということを指定できる class GenerateBoardSource() { fun generateBoard(seed: Long) : SudokuGame { StrictMode.noteSlowCall("Generating Sudoku board") return SudokuSolver.generate() } } StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() ... .detectCustomSlowCalls() ... .build())

InterProcess Communication (IPC)
  • 他のアプリを呼び出す場合、基本こちらに制御権がない
  • 呼び出し先が Network 処理や Disk 処理をするかもしれないので別スレッドで呼び出す


Locks and Synchronization
  • これはとても難しい問題
  • deadlock になったり main thread をブロックするかもしれない
  • デバッグが難しい
  • Android Vitals が提供する trace file の情報がデバッグに役立つかもしれない


Slow BroadcastReceiver handling
  • Android Manifest に BroadcastReceiver を登録した場合、onReceive メソッドは main thread で呼び出される
  • 実行に時間がかかる処理を onReceive() でやるべきではない
  • 10秒以内に処理を終えないと ANR になる
  • Notification に表示する画像を Disk から読み出すなどちょっとした Disk 処理が必要な場合は onReceive() で goAsync() を呼び、別のスレッドを立ち上げ、終わったら PendingResult.finish() を呼ぶ
  • https://developer.android.com/guide/components/broadcasts#effects-process-state
  • あまり長いと結局システムに kill されるので、長い処理が必要なら JobScheduler や WorkManager を使う



クラッシュ対策
  • クラッシュの対応として Activity のライフサイクルでむやみに null チェックや例外の握りつぶしをするべきではない
  • 車輪の再発明をしない : 問題を解決する利用できるライブラリを使う
    • Lifecycle handling (LiveData, ViewModel)
    • Database object mapping (Room)
    • Data paging (Paging)
    • *NEW* Fragment transitions, up/back, deep link handling (Navigation)
    • *New* Job scheduling (WorkManager)
  • 3rd party の優れたライブラリもたくさんある
  • Kotlin を使う : でも全てのクラッシュを防げる銀の弾丸ではないよ!
  • private / hidden API を使わない


バッテリー対策

停止した wake lock

wake lock が取得されたが適切に release されなかった
  • wake lock を使わない
  • 画面をつけっぱなしにしたいなら Activity の Window に FLAG_KEEP_SCREEN_ON を指定する
  • 自分で Service を管理せず job を schedule する
  • AlarmManager で BroadcastReceiver を起こすようにすると onReceive() の間 AlarmManager は wake lock を hold してしまう
  • wake lock を使わなければ permission も必要なくなる
  • どうしても wake lock を使わないといけないなら、常に PARTIAL_WAKE_LOCK を使うこと
  • wakeLock.acquire() にタイムアウトをセットすること
  • static な descriptive tag を渡すこと(Android Vitals でのデバッグがしやすくなる)
  • try { ... } finally { wakeLock.release() } すること


過度の wakeup
  • もっとも大きい原因は AlarmManager の *_WAKEUP アラーム
  • 可能ならなくす(Remove)
  • 頻度を減らす(Reduce)
  • FCM, WorkManager, JobScheduler, SyncManager などに置き換える(Replace)
  • Android Studio 3.2 Canary に追加された Energy Profiler で wake lock に関する問題をデバッグできる


関連