Jetpack Composeの実装思考
Jetpack Composeの最初のalphaがリリースされました。
Jetpack Composeはモダンな宣言的UIツールキットです。これまでのAndroidのViewシステムは異なるものになるため、実装においても考え方が大きく異なります。
React, Vue.js, Flutterなどで開発したことがある方なら、実装時に考えることや気をつけることが理解できてるかもしれません。しかし、これらを開発したことない方にとってはもしかしたら難しく感じるかもしれません。
(ぼくは、React, Vue.js, Flutterはちょっと触った程度の人間ですが…Vue.jsは仕事で書いたかな。)
ぼくが感じたJetpack Composeの実装においての考え方や気をつけたい箇所をまとめたいと思います。
(もしかしたら他の人と感じてることが違うかも?)
Single source of truth
Google I/O 2019, Android Dev Summit 2019 などの発表でも登場していたワードになります。
日本語に訳すと、信頼できる唯一の情報源
となるらしいですが、簡単に言うと、一箇所で状態を管理する感じです。
これまでのViewシステムにおいてはView自体も状態を持っているため、アプリケーションで管理している状態とViewの状態に矛盾が起きることがあります。
例えば CheckBox
で考えてみます。あるアプリケーションの状態(変数とか)を CheckBox
に常に反映してる場合であっても、 CheckBox
自体がチェックされてるかの状態を持っているため、ユーザーがチェックするとアプリケーションの状態と CheckBox
の状態に矛盾が起きます。
これはDataBinding (2-way binding) によってある程度は解決できてる部分もあるかなとは思いますが、矯正はできません。
Stateless
多くのビルトインのCompose UIは内部で状態を持っていません。
例えば、 TextField
( EditText
のような入力)であっても入力されてる値を内部で保持していません。
Single source of truthで書いたようにアプリケーションの状態とUIの状態で矛盾が生じるのを防ぐためです。
自分でComposableを作る場合にはStatefulに作ることも可能ですが、Statelessを保つことで再利用性も高まります。
また、ComposeではUIをオブジェクトとして取得することができないため、UIの値を参照するようなことも出来ません。例えば、 TextField
に入力されてる値を参照するようなことが出来なくなっています。
単一データフロー
こちらも以前から登場しているワードだと思います。MVVM/Fluxなんかもこの思想があります。
Jetpack Composeにおいてはデータは一方向にしか流れません。図にすると以下のような感じです。
上から下にしかデータが流れません。隣のViewにデータを渡したりすることももちろん出来ません。
次にボタンがタップされたなどの、ユーザーのイベントですが、これはデータフローとは逆になります。
発生したイベントの状態を上に引き上げていきます。 State hoisting
と言うらしいです。
Jetpack Composeでは以下のようなパターンをよく見ることになると思います。
表示したいテキストを引数で渡して(上から下のデータの流れ)、イベントが発生したときに関数の引数を使って上流にイベントを渡しています。
副作用
Composableは副作用を起こさないように実装しなければいけません。
副作用とはグローバル変数、SharedPreference、DB、Fileなどを更新するような他に影響を及ぼすようなことです。
同じ引数なら同じ振る舞いをするように、冪等にする必要があります。
これは後述するComposeの再構築で非常に重要になります。
もし何かしら副作用を起こしたい場合は、イベントコールバックなどを使って更新するようにします。
簡単な実装例(あまり良い例では無いかも)
ちょっとここまでの考えを踏まえて、簡単な実装例です。
単純なテキスト入力とボタンを設置して、ボタンが押されたらテキストの値をViewModelの処理へ渡しています。
細かくは説明しませんが、 Sample
というComposableを作っています。これは状態 ( remember
で値を保持) を持っていますので、Statelefulなものになります。
その配下の TextFiled
と Button
は状態を持っていない Statelessなものになります。こちらは再利用しやすいものになっています。
図にすると以下のような感じです。
このとき重要となるのが、 Button
のイベントでどうやって TextField
の値を参照しているかです。前述したとおりComposeのUIはオブジェクトとして参照することができません。
詳細は省略しますが、TextField
に入力されるたびにtextという変数が更新されるように実装しています。この変数を利用して現在入力されてる値を知ることができます。
textという変数が唯一の情報源となっている状態です。
またイベントも引数に関数を渡すことで上流へと通知されるようになっています。
(たぶん、この説明わかりにくいと思うので、Codelabなどをやったほうが分かりやすいと思います)
Recomposition
次に再構築についてです。Composeはパラメータに変更があった場合に再構築が行われます。 (Recomposition)
このときいくつか注意があります。基本的には副作用がないように冪等に実装していれば問題はないかと思います。
処理順
再構築おいて処理順は保証されていません。例えば、以下のように複数のComposable表示してる場合に、必ず上から再構築されるとは限りません。
MaterialTheme {
MyText()
MyInput()
MyButton()
}
この場合に MyText でグローバルな変数を変更していて、それを下の MyInput で参照している場合などは期待通りの動作をしない可能性があります。
並行
再構築は並行で実行することで最適化を行うことができます。この最適化によってバックグラウンドにて実行される可能性もあります。
スレッドセーフでない処理では問題が起きる可能性があります。
この例ではローカル変数の items を更新するため、処理のタイミングによっては最終的な値が変わってしまいます。
副作用のないコード見えますが、Columnから見るとグローバルな変数 (items) を変更しているので、副作用のあるコードとなってしまいます。
スキップ
再構築の必要のない箇所はスキップされる可能性があります。
Optimistic
Composeはパラメータが変更される前に再構築が完了することを期待しています。もし再構築が完了する前にパラメータが変更が変更されると一度キャンセルして再実行することになります。
頻繁に実行
アニメーションなどによって頻繁にComposableの関数が実行されることがあります。
Composable関数では負荷のかかる処理をしてしまうとパフォーマンスに影響が及ぼします。
まとめ
Jetpack Composeは非常に素晴らしいものですが、やはりこれまでのやり方と異なる点が多いため実装・設計について手に馴染むまで時間がかかりそうです。
ぼくもまだ触り始めたばかりですが、しっかりとドキュメントやブログを読んだり、Youtube動画を見たり、Codelabをやったりしていきたいなと思います。また、Jetpack Composeで何か作ってみようかなと。
最後に参考ドキュメントとCodelabを載せておきます。英語ですが読むことを強くおすすめします。今回書いた内容の元になっています。