既存のViewシステムのSpanと同様にJetpack Composeでもテキストの一部を装飾することができるので、実装方法を紹介します。
AnnotatedString
というのを使って実現します。
これを実行すると以下のような表示になります。
実装をみれば勘の良い方はすぐ理解できると思います。
buildAnnotatedString
関数のブロックで色々を設定していくことになります。実際には AnnotatedString.Builder
を扱いやすくしたものになります。
withStyle
を使ってスタイルを指定して、そのブロック内で装飾したい文字列を append
で追加していきます。
withStyle
を使わずに append
をした場合は、 Text
Composable関数で指定したスタイルが適用されます。
スタイルには SpanStyle
と ParagraphStyle
が使えます。違いとしては文字単位に適用するか、段落単位で適用するかになります。
テキストの装飾とあわせて、テキストの一部をクリックに対応したい場合があります。例えば、テキスト中にリンクがある場合などですね。
実装は以下のようになります。
実行結果は以下のようになり、Androidの箇所をクリックするとブラウザが開きます。
Jetpack Composeにおいて状態が変化したときにUIに反映するRecompositionという仕組みがあります。Recompositionでは変更された状態のみComposable関数を実行しますが、その判断にはソースコードの呼び出し箇所によって識別されます。
Listのようにループを使って実行する場合には、その順番とインスタンスからRecomposition対象なのかを判断します。そのためListの変更が起きた場合に不要なRecompositionがことがあります。
Listの変更によるRecompositionがどのように実行されるか、それを効率よく行う方法を見ていきます。
例として以下のようなコードで試してきます。
Todoというdata classのListを一覧表示する例です。
使用されてるListは以下のような感じでStateとして扱えるようにして、TodoListというComposable関数に渡している状態です。
こんな感じでListの末尾にデータが追加された場合についてです。
Androidのバージョンアップとともに、フルスクリーン画面を実現する方法が変更されてきたので、まとめておきます。
さらに次のAndroid 12でも一部変更が入りそうな気配です。
テーマ (Theme.Holo.NoActionBar.Fullscreen
)もしくは Window.setFlags
で実現していました。
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
ただ、この FLAG_FULLSCREEN
はAPI Level 30からはDeprecatedになっています。
Android 4.1以降では、 setSystemUiVisibility
を使って実現していました。
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
setSystemUiVisibility
ではステータスバーの他にもナビゲーションバーも非表示にすることができるようになっています。
Immersiveモードも導入されています。
setSystemUiVisibility
もAPI Level 30からはDeprecatedになっています。
上記のとおり、いずれの方法もDeprecatedになっているため、今後は別の方法で実現する必要があります。
Android 11 (API Level 30) から WindowInsetsController
というのが追加されています。今後はこちらを使って対応していくことになります。
binding.root.windowInsetsController?.run {
hide(WindowInsets.Type.systemBars())
}
hide
メソッドに隠したい WindowInsets.Type
を設定することで実現できます。上の systemBars
を使うとステータスバーとナビゲーションバーを両方を消すことが出来ます。
ナビゲーションバーは表示したままで、ステータスバーだけを消したい場合は、 hide(WindowInsets.Type.statusBars())
で実現できます。
あとから表示したい場合は show
メソッドを同じように使用することで再び表示できます。
実行するタイミングですが、 onCreate
等の一回しか実行されない箇所でやってしまうとホーム画面から戻ったときにそのまま表示されたままになるので、 onResume
等でやるほうが良いでしょう。
systemBarsBehavior
にてステータスバーやナビゲーションバーの再表示の挙動を指定することができます。
BEHAVIOR_SHOW_BARS_BY_TOUCH
: 画面をタップなどのユーザーが何かしら操作したときに表示されます。(デフォルト)BEHAVIOR_SHOW_BARS_BY_SWIPE
: 画面の上か下をスワイプすると表示されます。BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
: こちらも画面の上から下をスワイプすると表示されますが、表示後にしばらくすると自動でまた非表示になります。Sticky immersiveのような動きになります。これを書いてる時点ではAndroid 12はDeveloper Preview2ですが、すでに変更が入るようです。
systemBarsBehavior
に設定する値に変更が入ります。まだDeveloper Preiewなので更に変更があるかもですが、注意が必要です。
BEHAVIOR_SHOW_BARS_BY_SWIPE
はDeprecatedになり、 BEHAVIOR_DEFAULT
に変更され、BEHAVIOR_SHOW_BARS_BY_TOUCH
はDeprecatedなるため今後は使用しないほうがよくなります。
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
を使って実態を渡します。分かりにくいですが、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
に関わらず同じ順番で実行されます。
Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO