Photo by Andrey Metelev on Unsplash

Coroutines asyncは例外のハンドリングが結構ややこしくハマりどころだと思います。ハンドリングの方法を間違えるとクラッシュを引き起こすこともあります。

asyncを使う際の例外ハンドリングをまとめていきます。

SupervisorJobやsupervisorScopeなどの細かい解説はしてませんので、最後に載せてる参考リンク等で確認してください。

scope.async

CoroutineScopeから直接asyncを使う場合です。

次に複数のasyncを使った場合です。CoroutineScopeのJobによって挙動が変わります。

次に SupervisorJob を使った例です。

scope.launch内でのasync

CoroutineScopeからlaunchにて新たにCoroutineを起動して、その中でasyncを使う場合です。

awaitでtry catchしてる場合は、実はここも呼び出されますが、結局はasyncから親に伝播されてるので、意味がない感じになってしまいます。

最初に説明したscope.asyncの場合が問題ない理由としては、ルートのCoroutineになってるので伝搬する親がいないためです。

supervisorScopeで対応

これを対応するにはいくつか方法があります。まずはよく使われる supervisorScope で対応する方法です。

CoroutineExceptionHandlerで対応

もう一つよくある対応としては CoroutineExceptionHandler を使う方法があります。


Photo by Kind and Curious on Unsplash

Fragmentの使い方が誤っていないかをチェックする FragmentStrictMode について簡単に紹介します。

Fragment 1.4.0-alpha02で導入されています。
(今回は1.4.0-alpha03で検証しています。

android.os.StrictMode のFragment版という感じになります。

誤りを検出してLog出力する

簡単な例として、 <fragment> タグを使用してる場合にログ出力する実装です。

Policyについてはいくつかありますが、どういったのがあるかは後ほど紹介ます。

もし、レイアウトxmlで <fragment> を使ってる箇所がある場合は、以下のようなログが出力されるようになります。


Photo by La-Rel Easter on Unsplash

Fragment 1.4.0-alpha01 よりMultiple back stacksの対応が導入されました。この対応で新しいAPIが追加されてるので、使い方と振る舞いについて見ていきます。

おそらく、基本的に直接これらのAPIを使うことはなく Navigation Component を通して使うことが多いと思います。

注意

まだリリース直後のalpha01で試しているため、今後変更がある可能性が高いです。

新しいAPI

saveBackStackrestoreBackStack のAPIが新たに追加されていて、これらを使ってMultiple back stacksを実現していきます。

saveBackStack

こちらのAPIは addToBackStack で指定した名前を使用します。挙動としては popBackStack と同じような動作になりますが、あとから復元できるようにBack stackを保存します。

restoreBackStack

saveBackStack で保存したBack stackをあとから復元してFragmentを再表示します。

ドキュメントではBack stackを保存という記述になっていますが、個人的にはdetach/attachに近いのかなって思っています。

使用方法

使用例をいくつか見ながら説明していきます。

例1

で、そのあとに、 saveBackStack で Back stackを保存します。

supportFragmentManager.saveBackStack("backstack1")


Photo by Med Badr Chemmaoui on Unsplash

状態管理する方法は色々ありますが、UiStateという状態を一つのclassでまとめて管理する方法を紹介します。
(UiStateの他にはUiModel, ViewStateなど色んな呼び方を見かけます)

UiStateでも大きく2つやり方があるかなと思っています。
sealed class を使ったやり方と data class を使ったやり方です。

sealed class を使った管理

sealed class を使って画面が取り得る状態を定義して、必要なデータは各継承クラスのプロパティに持たせる方法です。

以下のコメントに書いてる通り、各状態を sealed class を継承した data classobject で表現しています。

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow

問題点

ぼく個人としてはこのやり方はいくつか問題点があり、あまりやりたくはない方法になります。

上の例で示したとおり、画面全体の状態を表しているので、ちょっと複雑な画面などすぐに破綻します。
例えば、画面にお気に入りボタンがあったりした場合などは、 sealed class の継承するものがどんどん増えていきます。

画面の更新についても、画面全体の状態と表すのでUIを更新する際に冗長になり、またミスが増え要因になるように思えます。
以下に例を示しますが、画面全体の状態ごとにすべてのUIの状態を設定する必要があります。

data class を使った状態管理

次に単純な data class を使って管理する方法です。

sealed class のように画面全体の表現するのではなく、UIを表現するのに必要なものをプロパティとして定義するイメージです。


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 についてまとめておきます。

構文

@JvmInline
value class UserId(val id: Int)

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

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


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

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

簡単な実装

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

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

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

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

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

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

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

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


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

Photo by Cristina Gottardi on Unsplash

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

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

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

実装例

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

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

Listの末尾にデータを追加


Photo by Erik Mclean on Unsplash

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

これまでの対応方法

Android 4.0 以下

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

window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)

Android 4.1以降、Android 10 以下

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

window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN

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

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

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

Android 11の対応方法

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

binding.root.windowInsetsController?.run {
hide(WindowInsets.Type.systemBars())
}

あとから表示したい場合は 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の情報を提供します。

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

もし、 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をつくります。


KSP (Kotlin Symbol Processing) を使ってコード生成する方法について簡単に説明していきます。まとまりがない感じになっちゃって長くなりましたが、ぜひ実際に動かしてもらえると良いかなと思います。

注意: これを書いてる時点ではAlphaリリースされたばかりなので変更される可能性が高いです

KSP (Kotlin Symbol Processing) とは?

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

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

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