Jetpack ComposeにおいてUIの下位階層にデータを渡すには、Composable関数に引数を渡していくのが一般的だと思います。
引数で渡す以外の方法として、CompositionLocal
があります。
これの使い方について解説します。
先に CompositionLocal
に関する注意事項を書いておきます。
CompositionLocal
はほとんどケースでは使わなくて良いものです。グローバル変数のように見えるため乱用するとメンテンスが難しくなったりバグを引き起こしやすくなったりする可能性があります。
一部ケースで有用なものですが、使用する際は十分に注意してください。
例として、User情報を下位階層に渡したい場合を実装していきます。
まず compositionLocalOf
を使ってCompositionLocal
のキーを作成します。
型パラメータとして下位階層に渡したい型を指定し、引数の関数にはデフォルトの値を指定します。この例では、もし渡されてなかった場合はエラーにしています。
次に CompositionLocalProvider
を使って実際にUserの情報を提供します。
compositionLocalOf
で作ったキーに対して provides
を使って実態を渡します。分かりにくいですが、provides
はinfix関数になっていて、 ProvidedValue
を生成するようになっています。
最後に渡された値を取得します。
上のようにキーのcurrentからいつでもUserの情報が取得できるようになります。
もし、 CompositionLocalProvider
で渡してない場合は、compositionLocalOf
で指定したデフォルトが使用されますが、今回はエラーになるようにしています。
特に意識しなくても使えるようになってる CompositionLocal
がいくつかあります。androidx.compose.ui.platform
にLocal~みたいな名前で定義されています。
https://developer.android.com/reference/kotlin/androidx/compose/ui/platform/package-summary
よく使いそうなものでいうと、 Contextが取得できる LocalContext
や、URLを開くことができる LocalUriHandler
があります。
https://developer.android.com/reference/kotlin/androidx/compose/runtime/CompositionLocal
ぼくが思いついた便利な使い方としてはNavigation Composeと組み合わせた使い方です。
まず画面遷移のメソッドを定義したinterfaceをつくります。
CompositionLocal
のキーを作ります。
先程のNavigatorを実装したクラスをつくります。
KSP (Kotlin Symbol Processing) を使ってコード生成する方法について簡単に説明していきます。まとまりがない感じになっちゃって長くなりましたが、ぜひ実際に動かしてもらえると良いかなと思います。
注意: これを書いてる時点ではAlphaリリースされたばかりなので変更される可能性が高いです
簡単に言うと、KSPはKAPTと似たような機能を提供しつつビルド速度が向上したものになります。
仕組みとしては、Kotlin Compiler Pluginのサブセットのようなものになっていて、Kotlin Compiler Pluginより簡単に実装できるようになっています。Kotlin Compiler Pluginではコードの改変なども出来たりしますが、KSPでコードファイルを追加するし …
ViewGroupには descendantFocusability
というものがあります。これはViewGroupのフォーカスの挙動を設定することができます。
通常のアプリではほとんど意識する必要はないのですが、例えばTVアプリのようにリモコン操作するようなアプリの場合は知っておくと良いかもです。
descendantFocusability には3つの設定値があります。
これによってどのような挙動が変わるかを見ていきます。
これはViewGroupの中の子Viewがフォーカスを取得できるかどうかによって動作が変わってきます。
最初に以下のようなレイアウトを見てみます。
基本ViewGroupはデフォルトで android:focusable=”false”
なので、フォーカスが当たるように true に設定します。
Buttonはデフォルトで android:focusable=”true”
になっています。この例では子Viewがフォーカスを受け取ることができる状態です。
このときは、ViewGroup(この例ではConstraintLayout)はフォーカスを当てることができません。 コードからrequestFocus
を使ったとしてもViewGroupではなく最初の子Viewのほうにフォーカスが当たります。
次に以下のレイアウトを見てみます。
今度は子Viewがフォーカスを取得でいない状態になっています。
このときはViewGroupのほうにフォーカスが当たるようになります。requestFocus
によってフォーカスを当てることも出来ます。
afterDescendantsをまとめると以下の挙動になります。
こちらはデフォルトの値になっています。
上の例ではわざと設定していますが、特に指定しなくても大丈夫です。
こちらは普通にフォーカスを取得することが可能な状態です。コードからrequestFocus
することでフォーカスを当てることも可能になっています。
ただ、リモコンなどによるフォーカス移動の場合は、レイアウトの配置などによっては受け取ったり受け取らなかったりします。
実行して実際に確認しないと分かりにくい部分でもあります。うまくフォーカスが当たらない場合などは、 android:nextFocusDown
などを使って明示的に指定するとよいと思います。
これは子Viewがフォーカスを取得できる状態であったとしても、フォーカスが当たらなくなります。
この場合、Buttonにフォーカスが当たらなくなります。 requestFocus
も効かなくなります。
もし、ViewGroupが android:focusable=”true”
の場合は、ViewGroup自体はフォーカスを取得できますが、子Viewについてはフォーカスは取得できません。
一括してViewGroupの子Viewのフォーカスを無効にしたいときなどは便利です。
複雑な画面の場合、フォーカス制御はかなり大変なものになりますが、このあたりをうまく使ってやっていくと良いと思います。
画面の引数などをDaggerのInjectと組み合わせて、ViewModelのコンストラクタに渡したい場合があったりします。
こういったケースではAssistedInjectを使うことで可能になります。
(他には SavedStateHandle を使う方法もあります。)
2.31のHiltを使っていきます。
以下のようなコンストラクタを持つViewModelを例にします。
1つ目は通常のDaggerモジュールなどに定義されたオブジェクトで、2つ目で SavedStateHandle
も受け取るようにします。最後に画面から渡したい引数になります。
今回は SavedStateHandle
も使ってますが、不要なら無くても大丈夫です。
画面から渡したいものを AssistedFactory として定義します。
画面からコンストラクタに渡したいものを引数にして、Injectしたいクラスが戻り値になってるメソッドを定義したinterfaceを作ります。
interfaceには @AssistedFactory
のアノテーションを追加します。
次にViewModelFactoryを定義しておきます。
今回は SavedStateHandle
も使ってるので AbstractSavedStateViewModelFactory
を使っています。もし不要なら ViewModelProvider.Factory
も使えます。
ViewModelのインスタンスは AssistedFactory のメソッドから作成して返します。
コンストラクタを AssistedInject の設定をします。
AssistedInjectしたい引数に @Assisted
を追加します。
ただし、通常のモジュールからInjectするものは無くて大丈夫です。
コンストラクタには @Inject
の代わりに @AssistedInject
を追加します。
最後に画面から値を渡すようにします。
今年もAndroid開発状況を個人的な観点からまとめたいと思います。
去年のはこちら
Kotlinはもう言うことは特にないですね。
個人的には KSP がどうなるかが気になるところですね。
Android11のリリースと合わせて正式にCoroutinesが推奨される非同期処理となりました。(あとAsyncTaskがDeprecatedになりましたね)
これまで以上にCoroutinesを使っていく場面が増えてくると思いますし、Jetpackライブラリでも当たり前のように使われていくと思います。
また、StateFlowやSharedFlowなどの便利なものも出てきています。
まだ触ったことない方はぜひチャレンジしてみてください。
Dagger AndroidからHiltを段階的に移行する方法です。最初のいくつかの設定をすれば段階的に移行することができると思います。
サンプルプロジェクトを実際に段階的に移行したので、各ステップごとにその差分を見ながら確認してもらえると良いかなと思います。
マルチモジュール構成のシンプルなサンプルになっています。
@Component.Factory
で、 @BindsInstance
で引数をもらってる場合は、この引数を使わないようにします。HiltではComponentが不要になるので、事前に対応しておきます。Context
についてはHiltでも扱えるので、これはそのままでも大丈夫です。
これに対応する方法としては、 DaggerのModuleからApplicationクラスにアクセ …
Fragmentのドキュメントが刷新されましたが、そこに次のように記載されています。
https://developer.android.com/guide/fragments/create#add-programmatic
Note: You should always use
setReorderingAllowed(true)
when performing aFragmentTransaction
. For more information on reordered transactions, see Fragment transactions.
setReorderingAllowed
を常に使うべきと記載されています。後方互換のためデフォルトではfalseになっています。
この setReorderingAllowed
によってライフサイクルがどのように変わるかを見ていきます。
使用したのは、 1.3.0-beta02 です。
implementation "androidx.fragment:fragment-ktx:1.3.0-beta02"
Fragmentの管理方法の内部実装が新しくなってるのですが、これにより setReorderingAllowed
の挙動も影響を受けます。
New State Managerについては以下の記事にあります。
Fragment 1.3.0-alpha08
からこちらはデフォルトで有効になっています。
以下のコードを先に実行しておくことで、以前の方法として実行することもできます。
FragmentManager.enableNewStateManager(false)
このNew State Mangerが有効、無効での挙動の違いも見ていきます。
この記事ではNew State Manger / Old State Manger と表現します。
以下のように単純にFragment遷移をするときの違いです。
FragmentA から FragmentBへ遷移します。
単純な遷移では特に影響はないと思いますが、 Shared element transitions などで postponeEnterTransition
を使う場合は必ず setReorderingAllowed(true)
にする必要があります。そうしないとアニメーションが奇妙な感じなります。
New State Mangerの場合は setReorderingAllowed
に関わらず同じ順番で実行されます。
ConcatAdapter
と GridLayoutManager
を使うときに、Adapterによって列数を可変にする方法です。
例えば、以下のような画面です。
実装は結構簡単にできます。
まず、各Adapterでは getItemViewType
を実装して他のAdapterとカブらない値を返します。一番良いのはレイアウトIDを返す方法だと思います。
次に GridLayoutManager
の設定です。 GridLayoutManager.SpanSizeLookup
を使って動的にSpanサイズを設定することができます。
ConcatAdapter
からpositionごとに viewType を取得して、SpanSizeを設定していきます。この例だと、 R.layout.item_one_column
の場合は1列表示で、その他の viewType は2列で表示する設定します。
これだけでは、期待通りの挙動にはなりません。
ConcatAdapter
はデフォルトでは viewType は Adapter ごとに0からインクリメントされた値を返します。
例えば、Adapterを3つ使ってる場合、ConcatAdapter
の viewType は0, 1, 2を返します。
このviewTypeを各Adapterで設定された値を返すようにするには、以下のように ConcatAdapter.Config
の設定をする必要があります。
isolateViewTypes
にfalseを設定することで、各Adapterで設定されたviewTypeを返してくれるようになります。
これだけで、複雑な GridLayoutManager
も実装することが可能になります。
isolateViewTypes
の本来の用途としては、ConcatAdapter
で使用されてる各AdapterでのViewHolderを共有するものになります。
詳しくは以下の記事を参照。
そのため、もし共有されて困るような場合があれば、別の方法が必要かもしれません。
ColorStateList
を使用するときの注意点です。いくつかハマったので残しておきます。まず、ColorStateList
に簡単に説明です。
res/color
にxmlファイルとして定義することが出来ます。
これをButtonの android:textColor
に設定することで、押したときにテキストの色を変更できます。
また、すこし別の使い方として、既存の色に android:alpha
の値を設定して新たに色の定義として使用することができます。
似たようなものに StateListDrawable
があります。こちらはColorではなくDrawableになります。
この ColorStateList
を使ったときの注意について解説していきます。
レイアウトXMLで設定するときには基本的に問題はならないですが、コードから設定するとき、且つ Android 5.x以下 の場合に注意が必要です。
例えば、以下のようにThemeのattributeを使って色を指定しているときです。 ?attr/colorPrimary
の箇所ですね。これは半透明にしてます。
このとき、 ContextCompat.getColorStateList
で取得して設定すると Android 5.x 以下の場合はThemeが反映されません。なので、期待している色を取得できません。
ドキュメントにも記載されていますが、23以降しかThemeが反映されません。
Starting in Build.VERSION_CODES.M, the returned color state list will be styled for the specified Context’s theme.
これを解決するには、AppCompatResources.getColorStateList
を使います。これを使うとThemeが反映された状態で色を取得するので、期待する色が取得できます。
Android 5.xのサポートは減ってきてるかもですが、使用する際は気をつけてください。
これはどのAndroidバージョンでも注意が必要です。
同じく半透明にする ColorStateList
を使います。
追記: まだまだ検討する必要があります。この記事は参考程度にしてもらえると助かります。
これまでViewModelからViewへのイベント通知を行うには、SingleLiveEvent
がよく使われてきました。
これに代わる手法を個人的に色々模索してたのですが、Coroutines 1.4で導入された SharedFlow
が使えるのではないかと思っています。
まだ実際にプロダクションに投入してるわけではないので、なにか見落としてることはあるかもです。そのときは指摘してもらえると嬉しいです。
SingleLiveEvent
は LiveData
を改造したものになり、そもそもの LiveData
の挙動から大きく異なるものになります。
こちらの IssueTracker ではそういった理由で Je …
Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO