Kotlin 1.5からStableになる value class についてまとめます。

Photo by Kira auf der Heide on Unsplash

Kotlin 1.2.30からあった inline class がこれまではExperimental/Betaでしたが、1.5でStableになり value class に変更になります。inline class もまだ使えますが警告が表示されます。

この value class についてまとめておきます。

構文

構文としては data class に似ています。 value class というのを使って宣言します。

value class には以下の制約があります。

  • 単一のプロパティしか持てない
  • mutableなプロパティを持てない ( var を使ったプロパティが宣言できない)
  • 参照の比較はできない ( === を使っ …


既存のViewシステムのSpanと同様にJetpack Composeでもテキストの一部を装飾することができるので、実装方法を紹介します。

AnnotatedString というのを使って実現します。

簡単な実装

これを実行すると以下のような表示になります。

実装をみれば勘の良い方はすぐ理解できると思います。

buildAnnotatedString 関数のブロックで色々を設定していくことになります。実際には AnnotatedString.Builder を扱いやすくしたものになります。

withStyle を使ってスタイルを指定して、そのブロック内で装飾したい文字列を append で追加していきます。

withStyleを使わずに appendをした場合は、 Text Composable関数で指定したスタイルが適用されます。

スタイルには SpanStyleParagraphStyle が使えます。違いとしては文字単位に適用するか、段落単位で適用するかになります。

テキストの一部をクリック可能にする

テキストの装飾とあわせて、テキストの一部をクリックに対応したい場合があります。例えば、テキスト中にリンクがある場合などですね。

実装は以下のようになります。

実行結果は以下のようになり、Androidの箇所をクリックするとブラウザが開きます。


Listの変更によるRecompositionがどのように実行されるか、それを効率よく行う方法を見ていきます。

Photo by Cristina Gottardi on Unsplash

Jetpack Composeにおいて状態が変化したときにUIに反映するRecompositionという仕組みがあります。Recompositionでは変更された状態のみComposable関数を実行しますが、その判断にはソースコードの呼び出し箇所によって識別されます。

Listのようにループを使って実行する場合には、その順番とインスタンスからRecomposition対象なのかを判断します。そのためListの変更が起きた場合に不要なRecompositionがことがあります。

Listの変更によるRecompositionがどのように実行されるか、それを効率よく行う方法を見ていきます。

実装例

例として以下のようなコードで試してきます。

Todoというdata classのListを一覧表示する例です。

使用されてるListは以下のような感じでStateとして扱えるようにして、TodoListというComposable関数に渡している状態です。

Listの末尾にデータを追加

こんな感じでListの末尾にデータが追加された場合についてです。


Photo by Erik Mclean on Unsplash

Androidのバージョンアップとともに、フルスクリーン画面を実現する方法が変更されてきたので、まとめておきます。
さらに次のAndroid 12でも一部変更が入りそうな気配です。

これまでの対応方法

Android 4.0 以下

テーマ (Theme.Holo.NoActionBar.Fullscreen)もしくは Window.setFlags で実現していました。

ただ、この FLAG_FULLSCREEN はAPI Level 30からはDeprecatedになっています。

Android 4.1以降、Android 10 以下

Android 4.1以降では、 setSystemUiVisibility を使って実現していました。

setSystemUiVisibility ではステータスバーの他にもナビゲーションバーも非表示にすることができるようになっています。

Immersiveモードも導入されています。

setSystemUiVisibility もAPI Level 30からはDeprecatedになっています。

上記のとおり、いずれの方法もDeprecatedになっているため、今後は別の方法で実現する必要があります。

Android 11の対応方法

Android 11 (API Level 30) から WindowInsetsController というのが追加されています。今後はこちらを使って対応していくことになります。

hide メソッドに隠したい WindowInsets.Type を設定することで実現できます。上の systemBars を使うとステータスバーとナビゲーションバーを両方を消すことが出来ます。
ナビゲーションバーは表示したままで、ステータスバーだけを消したい場合は、 hide(WindowInsets.Type.statusBars()) で実現できます。

あとから表示したい場合は show メソッドを同じように使用することで再び表示できます。

実行するタイミングですが、 onCreate 等の一回しか実行されない箇所でやってしまうとホーム画面から戻ったときにそのまま表示されたままになるので、 onResume 等でやるほうが良いでしょう。

systemBarsBehavior

systemBarsBehavior にてステータスバーやナビゲーションバーの再表示の挙動を指定することができます。

  • BEHAVIOR_SHOW_BARS_BY_TOUCH: 画面をタップなどのユーザーが何かしら操作したときに表示されます。(デフォルト)
  • BEHAVIOR_SHOW_BARS_BY_SWIPE: 画面の上か下をスワイプすると表示されます。
  • BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE: こちらも画面の上から下をスワイプすると表示されますが、表示後にしばらくすると自動でまた非表示になります。Sticky immersiveのような動きになります。

Android 12からの対応

これを書いてる時点ではAndroid 12はDeveloper Preview2ですが、すでに変更が入るようです。

systemBarsBehavior に設定する値に変更が入ります。まだDeveloper Preiewなので更に変更があるかもですが、注意が必要です。

BEHAVIOR_SHOW_BARS_BY_SWIPE はDeprecatedになり、 BEHAVIOR_DEFAULT に変更され、BEHAVIOR_SHOW_BARS_BY_TOUCH はDeprecatedなるため今後は使用しないほうがよくなります。

Jetpackの対応

androidx coreの1.5.0-alpha05から WindowInsetsController のバックポートである、 WindowInsetsControllerCompat が追加されています。

しかし、これを書いてる時点ではバグがあり、BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE が期待通りの挙動になりません。DeprecatedになったSYSTEM_UI_FLAG_IMMERSIVE の代わりにを使うようにドキュメントに記載されていますが、同様の挙動にはなっていないようです。

詳しくは こちらのIssue を見てください。

ドキュメント関連

WindowInsetsController のAPIドキュメントはありますが、これを書いてる時点では、ガイドはありませんでした。

古いガイドのほうは、まだ残っています。

https://developer.android.com/training/system-ui

参考


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でコードファイルを追加するし …


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 を追加します。

画面から値を渡す

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


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クラスにアクセ …

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