Jetpack ComposeにおいてUIの下位階層にデータを渡すには、Composable関数に引数を渡していくのが一般的だと思います。

引数で渡す以外の方法として、CompositionLocal があります。

これの使い方について解説します。

注意事項

先に CompositionLocal に関する注意事項を書いておきます。

CompositionLocal はほとんどケースでは使わなくて良いものです。グローバル変数のように見えるため乱用するとメンテンスが難しくなったりバグを引き起こしやすくなったりする可能性があります。

一部ケースで有用なものですが、使用する際は十分に注意してください。

実装方法

例として、User情報を下位階層に渡したい場合を実装していきます。

まず compositionLocalOf を使ってCompositionLocal のキーを作成します。

型パラメータとして下位階層に渡したい型を指定し、引数の関数にはデフォルトの値を指定します。この例では、もし渡されてなかった場合はエラーにしています。

次に CompositionLocalProvider を使って実際にUserの情報を提供します。

compositionLocalOf で作ったキーに対して provides を使って実態を渡します。分かりにくいですが、providesinfix関数になっていて、 ProvidedValue を生成するようになっています。

最後に渡された値を取得します。

上のようにキーのcurrentからいつでもUserの情報が取得できるようになります。

もし、 CompositionLocalProvider で渡してない場合は、compositionLocalOf で指定したデフォルトが使用されますが、今回はエラーになるようにしています。

提供されてるCompositionLocal

特に意識しなくても使えるようになってる 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 (Kotlin Symbol Processing) とは?

簡単に言うと、KSPはKAPTと似たような機能を提供しつつビルド速度が向上したものになります。

仕組みとしては、Kotlin Compiler Pluginのサブセットのようなものになっていて、Kotlin Compiler Pluginより簡単に実装できるようになっています。Kotlin Compiler Pluginではコードの改変なども出来たりしますが、KSPでコードファイルを追加するし …


Image for post
Image for post
Photo by Markus Spiske on Unsplash

ViewGroupには descendantFocusability というものがあります。これはViewGroupのフォーカスの挙動を設定することができます。

通常のアプリではほとんど意識する必要はないのですが、例えばTVアプリのようにリモコン操作するようなアプリの場合は知っておくと良いかもです。

descendantFocusability には3つの設定値があります。

  • afterDescendants
  • beforeDescendants
  • blocksDescendants

これによってどのような挙動が変わるかを見ていきます。

afterDescendants

これはViewGroupの中の子Viewがフォーカスを取得できるかどうかによって動作が変わってきます。

最初に以下のようなレイアウトを見てみます。

基本ViewGroupはデフォルトで android:focusable=”false” なので、フォーカスが当たるように true に設定します。
Buttonはデフォルトで android:focusable=”true” になっています。この例では子Viewがフォーカスを受け取ることができる状態です。

このときは、ViewGroup(この例ではConstraintLayout)はフォーカスを当てることができません。 コードからrequestFocus を使ったとしてもViewGroupではなく最初の子Viewのほうにフォーカスが当たります。

次に以下のレイアウトを見てみます。

今度は子Viewがフォーカスを取得でいない状態になっています。

このときはViewGroupのほうにフォーカスが当たるようになります。requestFocus によってフォーカスを当てることも出来ます。

afterDescendantsをまとめると以下の挙動になります。

  • 子Viewがフォーカスを受け取れる → ViewGroupは受け取れない
  • 子Viewがどれもフォーカスを受け取れない → ViewGroupは受け取れる

beforeDescendants

こちらはデフォルトの値になっています。

上の例ではわざと設定していますが、特に指定しなくても大丈夫です。

こちらは普通にフォーカスを取得することが可能な状態です。コードからrequestFocus することでフォーカスを当てることも可能になっています。

ただ、リモコンなどによるフォーカス移動の場合は、レイアウトの配置などによっては受け取ったり受け取らなかったりします。
実行して実際に確認しないと分かりにくい部分でもあります。うまくフォーカスが当たらない場合などは、 android:nextFocusDown などを使って明示的に指定するとよいと思います。

blocksDescendants

これは子Viewがフォーカスを取得できる状態であったとしても、フォーカスが当たらなくなります。

この場合、Buttonにフォーカスが当たらなくなります。 requestFocus も効かなくなります。

もし、ViewGroupが android:focusable=”true” の場合は、ViewGroup自体はフォーカスを取得できますが、子Viewについてはフォーカスは取得できません。

一括してViewGroupの子Viewのフォーカスを無効にしたいときなどは便利です。

複雑な画面の場合、フォーカス制御はかなり大変なものになりますが、このあたりをうまく使ってやっていくと良いと思います。


Dagger 2.31より、AssistedInjectのサポートが入りました。このAssistedInjectをViewModelで使用する方法です。

画面の引数などをDaggerのInjectと組み合わせて、ViewModelのコンストラクタに渡したい場合があったりします。
こういったケースではAssistedInjectを使うことで可能になります。
(他には SavedStateHandle を使う方法もあります。)

build.gradle

2.31のHiltを使っていきます。

ViewModel

以下のようなコンストラクタを持つViewModelを例にします。

1つ目は通常のDaggerモジュールなどに定義されたオブジェクトで、2つ目で SavedStateHandle も受け取るようにします。最後に画面から渡したい引数になります。

今回は SavedStateHandle も使ってますが、不要なら無くても大丈夫です。

AssistedFactory

画面から渡したいものを AssistedFactory として定義します。

画面からコンストラクタに渡したいものを引数にして、Injectしたいクラスが戻り値になってるメソッドを定義したinterfaceを作ります。

interfaceには @AssistedFactoryのアノテーションを追加します。

ViewModelFactory

次にViewModelFactoryを定義しておきます。

今回は SavedStateHandle も使ってるので AbstractSavedStateViewModelFactory を使っています。もし不要なら ViewModelProvider.Factory も使えます。

ViewModelのインスタンスは AssistedFactory のメソッドから作成して返します。

AssistedInject

コンストラクタを AssistedInject の設定をします。

AssistedInjectしたい引数に @Assisted を追加します。
ただし、通常のモジュールからInjectするものは無くて大丈夫です。

コンストラクタには @Inject の代わりに @AssistedInject を追加します。

画面から値を渡す

最後に画面から値を渡すようにします。


Image for post
Image for post
Photo by Moritz Knöringer on Unsplash

今年もAndroid開発状況を個人的な観点からまとめたいと思います。
去年のはこちら

Kotlin

Kotlinはもう言うことは特にないですね。

個人的には KSP がどうなるかが気になるところですね。

Coroutines

Android11のリリースと合わせて正式にCoroutinesが推奨される非同期処理となりました。(あとAsyncTaskがDeprecatedになりましたね)

これまで以上にCoroutinesを使っていく場面が増えてくると思いますし、Jetpackライブラリでも当たり前のように使われていくと思います。

また、StateFlowSharedFlowなどの便利なものも出てきています。

まだ触ったことない方はぜひチャレンジしてみてください。

https://developer.android.com/kotlin/c …


Dagger AndroidからHiltを段階的に移行する方法です。最初のいくつかの設定をすれば段階的に移行することができると思います。

サンプルプロジェクトを実際に段階的に移行したので、各ステップごとにその差分を見ながら確認してもらえると良いかなと思います。

サンプルプロジェクト

マルチモジュール構成のシンプルなサンプルになっています。

Componentの引数を不要にする

差分

@Component.Factory で、 @BindsInstance で引数をもらってる場合は、この引数を使わないようにします。HiltではComponentが不要になるので、事前に対応しておきます。
Context についてはHiltでも扱えるので、これはそのままでも大丈夫です。

これに対応する方法としては、 DaggerのModuleからApplicationクラスにアクセ …


Image for post
Image for post
Photo by Brett Jordan on Unsplash

Fragmentのドキュメントが刷新されましたが、そこに次のように記載されています。

https://developer.android.com/guide/fragments/create#add-programmatic

Note: You should always use setReorderingAllowed(true) when performing a FragmentTransaction. 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"

New State Manager

Fragmentの管理方法の内部実装が新しくなってるのですが、これにより setReorderingAllowed の挙動も影響を受けます。

New State Managerについては以下の記事にあります。

Fragment 1.3.0-alpha08からこちらはデフォルトで有効になっています。

以下のコードを先に実行しておくことで、以前の方法として実行することもできます。

FragmentManager.enableNewStateManager(false)

このNew State Mangerが有効、無効での挙動の違いも見ていきます。

この記事ではNew State Manger / Old State Manger と表現します。

単純なFragment遷移

以下のように単純にFragment遷移をするときの違いです。
FragmentA から FragmentBへ遷移します。

単純な遷移では特に影響はないと思いますが、 Shared element transitions などで postponeEnterTransition を使う場合は必ず setReorderingAllowed(true) にする必要があります。そうしないとアニメーションが奇妙な感じなります。

New State Mangerの場合

New State Mangerの場合は setReorderingAllowed に関わらず同じ順番で実行されます。


ConcatAdapterGridLayoutManager を使うときに、Adapterによって列数を可変にする方法です。

例えば、以下のような画面です。

Image for post
Image for post

実装方法

実装は結構簡単にできます。

getItemViewType

まず、各Adapterでは getItemViewType を実装して他のAdapterとカブらない値を返します。一番良いのはレイアウトIDを返す方法だと思います。

GridLayoutManager.SpanSizeLookup

次に GridLayoutManager の設定です。 GridLayoutManager.SpanSizeLookup を使って動的にSpanサイズを設定することができます。

ConcatAdapter からpositionごとに viewType を取得して、SpanSizeを設定していきます。この例だと、 R.layout.item_one_column の場合は1列表示で、その他の viewType は2列で表示する設定します。

isolateViewTypes

これだけでは、期待通りの挙動にはなりません。

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について

まず、ColorStateList に簡単に説明です。

res/color にxmlファイルとして定義することが出来ます。

これをButtonの android:textColor に設定することで、押したときにテキストの色を変更できます。

また、すこし別の使い方として、既存の色に android:alpha の値を設定して新たに色の定義として使用することができます。

似たようなものに StateListDrawable があります。こちらはColorではなくDrawableになります。

この ColorStateList を使ったときの注意について解説していきます。

コードから設定するとき (Android 5.x以下)

レイアウト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のサポートは減ってきてるかもですが、使用する際は気をつけてください。

Backgroundに設定する場合

これはどのAndroidバージョンでも注意が必要です。

同じく半透明にする ColorStateList を使います。


SingleLiveEventの代わりにSharedFlowを使ってイベント通知

Image for post
Image for post
Photo by Adam Solomon on Unsplash

追記: まだまだ検討する必要があります。この記事は参考程度にしてもらえると助かります。

これまでViewModelからViewへのイベント通知を行うには、SingleLiveEvent がよく使われてきました。

これに代わる手法を個人的に色々模索してたのですが、Coroutines 1.4で導入された SharedFlow が使えるのではないかと思っています。
まだ実際にプロダクションに投入してるわけではないので、なにか見落としてることはあるかもです。そのときは指摘してもらえると嬉しいです。

SingleLiveEventの問題点

SingleLiveEventLiveData を改造したものになり、そもそもの LiveData の挙動から大きく異なるものになります。

こちらの IssueTracker ではそういった理由で Je …

Kenji Abe

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store