Save 37% off PRO during our Black Friday Sale! »

Inside Jetpack Compose

7166bc2cbc462ab5fd1987a76d0fe709?s=47 takahirom
October 21, 2021

Inside Jetpack Compose

7166bc2cbc462ab5fd1987a76d0fe709?s=128

takahirom

October 21, 2021
Tweet

Transcript

  1. Inside Jetpack Compose takahirom

  2. 自分について  takahirom • Takahiro Menju • Androidに関する技術が好き • DroidKaigi co-organizer

    • Google Developers Expert for Android • CyberAgent.Inc ABEMA • Twitter takahirom (@new_runnable) • GitHub takahirom
  3. コードが多いので ブログもご参照ください Inside Jetpack Compose https://qiita.com/takahirom/items/78e8ac1cf3 82a0a79a9f English version: https://medium.com/@takahirom/inside-jetpa

    ck-compose-2e971675e55e
  4. なぜ内部を知るか?

  5. Jetpack Composeは まるで魔法みたいに動く 関数に返り値がなくてもレイアウトされ たり、 勝手に差分更新がうまく動いたりしま す。 https://d.android.com/jetpack/comp ose より

  6. なぜ知るか? • 中身を知りたくなったのが純粋な理由です。 • これからJetpack ComposeがAndroid開 発のデファクトスタンダードになっていくの で、知っていると活躍できるかも? • 調べて分かったのは

    Jetpack Composeの 中身はかなり面白いので ぜひこのセッションを通して知ってほしい! https://d.android.com/jetpack/comp ose より
  7. どうやって内部を知るか?

  8. 普通に見るかなり複雑 Androidとの連携のコードがかなりたく さん書かれており、 それだけでもComposeの仕組みがか なり動いてしまうので、 仕組みを知っていくことが難しい https://cs.android.com/androidx/platform/frameworks/support/+/androidx- main:compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform /Wrapper.android.kt;drc=196281eb061d6e3eb1ad2f05b4cb8c5e5fbdd70 f

    より
  9. Jetpack Composeは Android以外でも使える WebやDesktopでも同じ仕組みを使っ ているプロダクトが存在する Compose for Web Compose for

    Desktop Mosaic (Compose for console UI) すごくシンプルな Jetpack Composeを使うツールを 用意することで 仕組みを知りやすくできるのでは?
  10. シンプルにJetpack Composeを使うコードの 紹介 https://github.com/takahirom/simple-compose-for-learning-inside-compose

  11. シンプルに木構造をコンソールに出力する 3秒後 https://github.com/takahirom/simple-compose-for-learning-inside-compose

  12. シンプルに木構造をコンソールに出力するのに必要な コードリスト この3秒後に消えるNode1とNode2を呼び出すComposable関数 (ここだけ見られれば本筋的にはOK) Nodeクラス Composeの木構造に関する操作を適用するクラス NodeをComposeにEmitするComposable関数 上記をつなげて動かすためのコード 01 02

    03 04 05 こう見るとすごく多く見えますが、合計 130行ぐらいのコードなので、 頑張ってください🙏 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  13. シンプルに木構造をコンソールに出力するのに必要な コードリスト この3秒後に消えるNode1とNode2を呼び出すComposable関数 (ここだけ見られれば本筋的にはOK) Nodeクラス Composeの木構造に関する操作を適用するクラス NodeをComposeにEmitするComposable関数 上記をつなげて動かすためのコード 01 02

    03 04 05 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  14. シンプルに木構造をコンソールに出力する このコードを Jetpack Composeの中身に 触れずに解説していきます https://github.com/takahirom/simple-compose-for-learning-inside-compose

  15. シンプルに木構造をコンソールに出力する Content()関数は `@Composable` がついている。つまり Composable 関数になっている。 https://github.com/takahirom/simple-compose-for-learning-inside-compose

  16. シンプルに木構造をコンソールに出力する まず trueのMutableStateを作る https://github.com/takahirom/simple-compose-for-learning-inside-compose

  17. シンプルに木構造をコンソールに出力する LaunchedEffect{}によりKotlin Coroutinesによる非同期処理を実行。 3秒後にstateをfalseに変更するが 起動直後は何も起こらない。 https://github.com/takahirom/simple-compose-for-learning-inside-compose

  18. シンプルに木構造をコンソールに出力する 最初はstate = trueなので、 Node1()とNode2()が両方とも動く https://github.com/takahirom/simple-compose-for-learning-inside-compose

  19. シンプルに木構造をコンソールに出力する 3秒後 stateがfalseに変更される https://github.com/takahirom/simple-compose-for-learning-inside-compose

  20. シンプルに木構造をコンソールに出力する 今度はstateがfalseになっているので Node2()だけが実行される。 つまりNode1()が消える https://github.com/takahirom/simple-compose-for-learning-inside-compose

  21. シンプルに木構造をコンソールに出力する • stateの変更をどうやって検知している の?🤔 • 変更があったときに再実行が必要な場 所ってどうやって見つけているの? 🤔 • MutableStateを非同期で

    変更したら可変ステートだから危ないん じゃないの?🤔 • Node2()もう一回実行されちゃいそうだけ ど差分で実行されてないんじゃない? 🤔 → あとで説明します まずはJetpack Composeで このコードを動かすための他の部品について説 明します。 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  22. この3秒後に消えるNode1とNode2を呼び出すComposable関数 (ここだけ見られれば本筋的にはOK) Nodeクラス Composeの木構造に関する操作を適用するクラス NodeをComposeにEmitするComposable関数 上記をつなげて動かすためのコード シンプルに木構造をコンソールに出力するのに必要な コードリスト 01 02

    03 04 05 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  23. シンプルに木構造をコンソールに出力する に必要なコードたち Nodeクラス • 木構造を作るので、そのための Nodeク ラスを用意します。 • ただのシンプルなクラスで、何も Jetpack

    Composeのクラスなどを継承したりして いません。 • 子ノードへの参照(children)を持っていて 木が作れるようになっています。 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  24. この3秒後に消えるNode1とNode2を呼び出すComposable関数 (ここだけ見られれば本筋的にはOK) Nodeクラス Composeの木構造に関する操作を適用するクラス NodeをComposeにEmitするComposable関数 上記をつなげて動かすためのコード シンプルに木構造をコンソールに出力するのに必要な コードリスト 01 02

    03 04 05 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  25. シンプルに木構造をコンソールに出力する に必要なコードたち Applier • Composeの木構造に関する操作を適用 するクラス。 • AbstractApplierを継承して作成する • 例えばNodeの親に子を追加したり、削除

    したりなどの操作を定義。 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  26. この3秒後に消えるNode1とNode2を呼び出すComposable関数 (ここだけ見られれば本筋的にはOK) Nodeクラス Composeの木構造に関する操作を適用するクラス NodeをComposeにEmitするComposable関数 上記をつなげて動かすためのコード シンプルに木構造をコンソールに出力するのに必要な コードリスト 01 02

    03 04 05 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  27. シンプルに木構造をコンソールに出力する に必要なコードたち NodeをComposeにEmitするComposable関数 • NodeをEmitするとComposeの中でNodeが 追加、管理される • ReusableComposableNode()というものを使 う ◦

    “Reusable” については ”ReusableContent”(LazyColumnなど で利用される)とセットで 意味を成すとみられるので、 一旦無視。 • ReusableComposableNode()の引数 • Nodeを作るfactoryを渡す • 変更があったときに利用するラムダを updateで渡す • ここで先程作ったNodeApplierを指定する • 同様のコードをNode2に対してもにも書く https://github.com/takahirom/simple-compose-for-learning-inside-compose
  28. この3秒後に消えるNode1とNode2を呼び出すComposable関数 (ここだけ見られれば本筋的にはOK) Nodeクラス Composeの木構造に関する操作を適用するクラス NodeをComposeにEmitするComposable関数 上記をつなげて動かすためのコード シンプルに木構造をコンソールに出力するのに必要な コードリスト 01 02

    03 04 05 https://github.com/takahirom/simple-compose-for-learning-inside-compose
  29. シンプルに木構造をコンソールに出力する に必要なコードたち これらをつなげて動かすコード ここではSnapshotやapplyChangeなどという 単語が出てくる。 あまり説明しませんが、これは後述で分かって くるはずです。 https://github.com/takahirom/simple-compose-for-learning-inside-compose

  30. このサンプルアプリを使って知っていこう

  31. ステップを追って見ていこう 概要図 https://github.com/takahirom/inside-jetpack-co mpose-diagram

  32. ステップを追って見ていこう 0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする 1. Composable関数を呼び出して、情報を SlotTableに格納する 2.

    3秒後のMutableStateへの変更 3. Snapshot Systemが変更をキャッチ 4. Recompose 5. GapBufferを使ったSlotTableへの反映 ビルド時 : 実行時: https://github.com/takahirom/inside-jetpack-compose-diagram
  33. ステップ0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする

  34. ステップ0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする 0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする

    1. Composable関数を呼び出して、情報を SlotTableに格納する 2. 3秒後のMutableStateへの変更 3. Snapshot Systemが変更をキャッチ 4. Recompose 5. GapBufferを使ったSlotTableへの反映 ビルド時 : 実行時: https://github.com/takahirom/inside-jetpack-compose-diagram
  35. ステップ0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする Kotlin Compiler Pluginとは? Androidの開発でKotlinを使う場合は、 KotlinをJavaバイトコードに変換し、その

    Javaバイトコー ドをdex形式に変換します。
  36. ステップ0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする 最近KotlinコンパイラではJavaバイトコードに変換する とき一度Kotlin IRと呼ばれる中間表現に変換するよう になりました。 このKotlin

    IRをKotlin Compiler Pluginで書き換えるこ とができます。 これによってJavaScriptなどにも変換できるのでマルチ プラットフォームにも対応できます。
  37. ステップ0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする コンパイル後のJava Bytecodeをデコンパイルするこ とでCompose Compiler Pluginの書き換えを見るこ

    とができます。 かんたんにこれができるライブラリも公開しています https://github.com/takahirom/decomposer
  38. Jetpack Composeは差分更新ができるので、差分を計 算するための元の情報を持っておかないといけないです よね? そのための情報がComposable関数によって SlotTableというもの(後述)に保存されます。 そのためにSlotTableを作れるように変更を加えます。 (Compiler Pluginは他にもいろいろやります。 )

  39. Compose Compiler Pluginによって変換された コードを軽く見てみる

  40. startRestartGroup() endRestartGroup() なにやらGroupというものを定義していそ う?

  41. 関数にもともと書かれていた処理は、 このelseの中に書いて有りそう 変換されたNode1()やNode2()内でも startRestartGroup()などがあるので、木 構造になりそう。

  42. if文で入れば、 どうやらもともとの処理はスキップできそ う?

  43. 再度呼び出し用のラムダもありそう

  44. ステップ1. Composable関数を呼び出して、情報をSlotTable に格納する

  45. ステップ1. Composable関数を呼び出して、情報を SlotTableに格納する 0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする 1. Composable関数を呼び出して、情報を

    SlotTableに格納する 2. 3秒後のMutableStateへの変更 3. Snapshot Systemが変更をキャッチ 4. Recompose 5. GapBufferを使ったSlotTableへの反映 ビルド時 : 実行時: https://github.com/takahirom/inside-jetpack-compose-diagram
  46. ステップ1. Composable関数を呼び出して、情報を SlotTableに格納する さて、ここでアプリが実際に起動して、先程の Content()などの関数が実行されます。 今回は差分更新について詳しく見ていきたいのでここは詳しく説明しません。 先程言ったとおり、Jetpack Composeは差分更新ができるので、差分を計算するための元 の情報を持っておかないといけないですよね? そのための情報がComposable関数によってSlotTableというものに保存されます。

  47. 少しSlotTableについて知る

  48. SlotTable 基本的に以下2つのデータ構造のみでうまく動く groups: IntArray slots: Array<Any?> IntArrayとAny型のArrayでどうやって動くのか??

  49. SlotTableはIntArrayとAny型のArrayでどうやって動く のか?? groups: IntArrayは5個一区切りで 格納されている • groups[0]からgroups[4]までが 1つ目のグループ • groups[5]からgroups[9]までが

    2つ目のグループ このようにするといい感じに中身を見られる どんな中身になっているか?
  50. SlotTableのgroupsは何が入っているか? index: 0, key: 100, groupInfo: 2, parentAnchor: -1, size:

    16, dataAnchor: 0 index: 1, key: 1000, groupInfo: 2, parentAnchor: 0, size: 15, dataAnchor: 1 index: 2, key: 200, groupInfo: 536870914, parentAnchor: 1, size: 14, dataAnchor: 1 index: 3, key: -985533309, groupInfo: 2, parentAnchor: 2, size: 13, dataAnchor: 2 index: 4, key: -337788314, groupInfo: 268435458, parentAnchor: 3, size: 12, dataAnchor: 4 index: 5, key: -3687241, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 6 index: 6, key: -3686930, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 8 index: 7, key: 1036442245, groupInfo: 268435456, parentAnchor: 4, size: 2, dataAnchor: 11 index: 8, key: -3686930, groupInfo: 268435456, parentAnchor: 7, size: 1, dataAnchor: 12 index: 9, key: -337788167, groupInfo: 1, parentAnchor: 4, size: 4, dataAnchor: 15 index: 10, key: 1815931657, groupInfo: 1, parentAnchor: 9, size: 3, dataAnchor: 15 index: 11, key: 1546164276, groupInfo: 268435457, parentAnchor: 10, size: 2, dataAnchor: 16 index: 12, key: 125, groupInfo: 1073741824, parentAnchor: 11, size: 1, dataAnchor: 17 index: 13, key: 1815931930, groupInfo: 1, parentAnchor: 4, size: 3, dataAnchor: 19 index: 14, key: 1546164276, groupInfo: 268435457, parentAnchor: 13, size: 2, dataAnchor: 20 index: 15, key: 125, groupInfo: 1073741824, parentAnchor: 14, size: 1, dataAnchor: 21 index: 16, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 17, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 18, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 19, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 20, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 21, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 22, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 23, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 1行1行が1グループになっている
  51. SlotTableのgroupsは何が入っているか? index: 0, key: 100, groupInfo: 2, parentAnchor: -1, size:

    16, dataAnchor: 0 index: 1, key: 1000, groupInfo: 2, parentAnchor: 0, size: 15, dataAnchor: 1 index: 2, key: 200, groupInfo: 536870914, parentAnchor: 1, size: 14, dataAnchor: 1 index: 3, key: -985533309, groupInfo: 2, parentAnchor: 2, size: 13, dataAnchor: 2 index: 4, key: -337788314, groupInfo: 268435458, parentAnchor: 3, size: 12, dataAnchor: 4 index: 5, key: -3687241, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 6 index: 6, key: -3686930, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 8 index: 7, key: 1036442245, groupInfo: 268435456, parentAnchor: 4, size: 2, dataAnchor: 11 index: 8, key: -3686930, groupInfo: 268435456, parentAnchor: 7, size: 1, dataAnchor: 12 index: 9, key: -337788167, groupInfo: 1, parentAnchor: 4, size: 4, dataAnchor: 15 index: 10, key: 1815931657, groupInfo: 1, parentAnchor: 9, size: 3, dataAnchor: 15 index: 11, key: 1546164276, groupInfo: 268435457, parentAnchor: 10, size: 2, dataAnchor: 16 index: 12, key: 125, groupInfo: 1073741824, parentAnchor: 11, size: 1, dataAnchor: 17 index: 13, key: 1815931930, groupInfo: 1, parentAnchor: 4, size: 3, dataAnchor: 19 index: 14, key: 1546164276, groupInfo: 268435457, parentAnchor: 13, size: 2, dataAnchor: 20 index: 15, key: 125, groupInfo: 1073741824, parentAnchor: 14, size: 1, dataAnchor: 21 index: 16, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 17, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 18, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 19, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 20, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 21, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 22, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 23, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 親を指し示すAnchorを持っているので 木構造が作られている。
  52. SlotTableのgroupsは何が入っているか? index: 0, key: 100, groupInfo: 2, parentAnchor: -1, size:

    16, dataAnchor: 0 index: 1, key: 1000, groupInfo: 2, parentAnchor: 0, size: 15, dataAnchor: 1 index: 2, key: 200, groupInfo: 536870914, parentAnchor: 1, size: 14, dataAnchor: 1 index: 3, key: -985533309, groupInfo: 2, parentAnchor: 2, size: 13, dataAnchor: 2 index: 4, key: -337788314, groupInfo: 268435458, parentAnchor: 3, size: 12, dataAnchor: 4 index: 5, key: -3687241, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 6 index: 6, key: -3686930, groupInfo: 268435456, parentAnchor: 4, size: 1, dataAnchor: 8 index: 7, key: 1036442245, groupInfo: 268435456, parentAnchor: 4, size: 2, dataAnchor: 11 index: 8, key: -3686930, groupInfo: 268435456, parentAnchor: 7, size: 1, dataAnchor: 12 index: 9, key: -337788167, groupInfo: 1, parentAnchor: 4, size: 4, dataAnchor: 15 index: 10, key: 1815931657, groupInfo: 1, parentAnchor: 9, size: 3, dataAnchor: 15 index: 11, key: 1546164276, groupInfo: 268435457, parentAnchor: 10, size: 2, dataAnchor: 16 index: 12, key: 125, groupInfo: 1073741824, parentAnchor: 11, size: 1, dataAnchor: 17 index: 13, key: 1815931930, groupInfo: 1, parentAnchor: 4, size: 3, dataAnchor: 19 index: 14, key: 1546164276, groupInfo: 268435457, parentAnchor: 13, size: 2, dataAnchor: 20 index: 15, key: 125, groupInfo: 1073741824, parentAnchor: 14, size: 1, dataAnchor: 21 index: 16, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 17, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 18, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 19, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 20, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 21, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 22, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 index: 23, key: 0, groupInfo: 0, parentAnchor: 0, size: 0, dataAnchor: 0 Slotのデータを指し示す dataAnchorを 持っている slots: Arrray<Any?>
  53. SlotTable#asString() Group(0) key=100, nodes=2, size=16, slots=[0: {}] Group(1) key=1000, nodes=2,

    size=15 Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider) Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@4fb4ae6, androidx.compose.runtime.i Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@b882ad4] Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@167707773] Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@167707773, Fun Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.Lau Group(9) key=-337788167, nodes=1, size=4 Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@7421fc3] Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1] Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@81cf51f] Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2] SlotTable#asString()を呼ぶと木構造が分かる文字列を 作って返してくれる このSlotTableというクラスに上記データが起動後に保持される
  54. ステップ 2. 3秒後のMutableStateへの変更 ステップ 3. Snapshot Systemが変更をキャッチ

  55. 0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする 1. Composable関数を呼び出して、情報を SlotTableに格納する 2. 3秒後のMutableStateへの変更

    3. Snapshot Systemが変更をキャッチ 4. Recompose 5. GapBufferを使ったSlotTableへの反映 ビルド時 : 実行時: ステップ 2. 3秒後のMutableStateへの変更 ステップ 3. Snapshot Systemが変更をキャッチ https://github.com/takahirom/inside-jetpack-compose-diagram
  56. 3秒後、このstateをfalseに書き換えます このbyっていうのはどういうものでしょうか、 remember{}とは? mutableStateOf(true) とは? ステップ 2. 3秒後のMutableStateへの変更 ステップ 3.

    Snapshot Systemが変更をキャッチ
  57. ステップ 2. 3秒後のMutableStateへの変更 ステップ 3. Snapshot Systemが変更をキャッチ `remember{}`はインスタンスの生存期間を伸ばしてくれ るもので、 `by`

    はKotlinのDelegated Propertyによるも ので、 細かいところは違いますが、基本的には同じです。 だいたい同じ LaunchedEffectによる別の場所からの MutableStateの変更を どのようにJetpack Composeがキャッチしているのか?
  58. Snapshot Systemについて学ぼう

  59. Snapshot Systemについて学ぼう このようなコードがあったとします。 ViewModelでstateを持っていて変えるだけです。

  60. Snapshot Systemについて学ぼう フレーム間の変更をどのように検知するか? Snapshot Systemを使ってComposeは変更 を検知しています。 → 何も出力されない ComposeのコンパイラなしでComposeの Runtimeを使うことで、Snapshotを使って遊

    ぶことができます。 ここではSnapshot.registerApplyObserver() という関数を使ってみましょう。 次のコードでは何が出力されるでしょうか?
  61. Snapshot Systemについて学ぼう フレーム間の変更をどのように検知するか? Snapshot.sendApplyNotifications()を追加すること でregisterApplyObserver()で渡しているapply observerが反応します 出力

  62. Snapshot Systemについて学ぼう この仕組みを使って Jetpack Composeは フレームごとに溜まった変更を処理していきます。 LaunchedEffect{}内でのMutableStateへの変更もここ で見つかります。 フレームの間はいいとして Composeが実際にRecompose中、

    つまりContent()などを呼んでいる時に別スレッドから MutableState が変更されたらどうなってしまうのか? 実際のComposeのコードで変更を受け取るデバッグ画面 フレームごとに溜まった変更を 処理できそう!
  63. Snapshot Systemについて学ぼう 別スレッドからの変更をどう解決するか? 実は今までの例ではトップレベルに保持されている GlobalSnapshotを使っていましたが、 GlobalSnapshot 内にSnapshotを作ることができます。 ComposeはContent()などを呼び出しなおす Recomposeをする前に Snapshot.takeMutableSnapshot()でSnapshotを作

    成し、 そのsnapshot内(Snapshot.enter{}内)で Recomposeします。Snapshotはゲームのセーブ ポイントだと思っていただければ大丈夫です。 ちょっと実験してみよう
  64. 別スレッドからの変更をどう解決するか? MutableStateの書き換え

  65. 別スレッドからの変更をどう解決するか? ここでSnapshotを撮る

  66. 別スレッドからの変更をどう解決するか? Snapshotを撮った後に別スレッドで変更

  67. 別スレッドからの変更をどう解決するか? snapshot.enter{}を呼ぶ

  68. 別スレッドからの変更をどう解決するか? さて結果は?

  69. 別スレッドからの変更をどう解決するか? 明らかに別スレッドから書き換えられていそうだ が、Snapshot内つまり、Snapshot#enter()の中 では、Snapshot取得時の値になっている 出力 では、Snapshot内(Snapshot#enter())内でも書き換 えて、別スレッドからも書き換えて、 同時に書き換えたらどうなるのか??

  70. では、Snapshot内でも書き換えて、 同時に書き換えたらどうなるのか?? 別スレッドでの書き換え

  71. では、Snapshot内でも書き換えて、 同時に書き換えたらどうなるのか?? Snapshot内での書き換え

  72. では、Snapshot内でも書き換えて、 同時に書き換えたらどうなるのか?? snapshot.apply()を呼ぶことで、 GlobalSnapshotに結果を反映する

  73. では、Snapshot内でも書き換えて、 同時に書き換えたらどうなるのか?? さて結果は?

  74. では、Snapshot内でも書き換えて、 同時に書き換えたらどうなるのか?? エラーなく実行されます。 そして最終的に別スレッドの変更が勝った みたいです。 こういったコンフリクトする変更は勝手に処理され る??

  75. コンフリクトした変更は勝手に 処理される?? 実はmutableStateOf()には SnapshotMutationPolicyを 渡せて、自分でマージのとき の動きを実装できます。 変更が検知、うまく反映されることは分かったが、どうやって 変更があったComposable関数をComposeは知るのか?

  76. Snapshot Systemについて学ぼう Composeは変更があったMutableStateを見ているComposable関数を見つけて、再度呼び出し (Reompose)をします。 これをどのように行うでしょうか? MutableState.valueでreadしたときのタイミングが分かっただけ で、引数とかでスコープが渡ってきたりしないけど、 どうやって再度呼び出しするスコープを見つけるの?? Snapshot.takeMutableSnapshot()は引数にreadObserverを渡すことができます。 これはMutableStateのgetValueが呼ばれたときに呼ばれます。これをうまく使います。

  77. Snapshot Systemについて学ぼう currentScopeを変数として持っておく どのように変更があった MutableStateを見ているComposable関 数をComposeは見つけるのか?

  78. Snapshot Systemについて学ぼう readObserverを渡して、MutableState.getValue()を呼ん だときに、ここが呼ばれるようにします。 (内部の処理についてはのちほど。 ) どのように変更があった MutableStateを見ているComposable関 数をComposeは見つけるのか?

  79. Snapshot Systemについて学ぼう currentScopeを変更していく どのように変更があった MutableStateを見ているComposable関 数をComposeは見つけるのか?

  80. Snapshot Systemについて学ぼう MutableState.getValue()を呼び出す。 どのように変更があった MutableStateを見ているComposable関 数をComposeは見つけるのか?

  81. Snapshot Systemについて学ぼう “変更があったオブジェクト ” to “currentScopeの文字列”の Mapを保存していく。 どのように変更があった MutableStateを見ているComposable関 数をComposeは見つけるのか?

  82. Snapshot Systemについて学ぼう 以下のようにどこのスコープがどの MutableStateを見ている のかが分かるログとして出力される! どのように変更があった MutableStateを見ているComposable関 数をComposeは見つけるのか? これでMutableStateを受け取る場所 が取得できました。

  83. Snapshot Systemについて学ぼう どのように変更があった MutableStateを見ているComposable関数をComposeは見つけるのか? 実際のComposeのコードでも、observationsにMutableStateに対してScopeがセットされます。

  84. Snapshot Systemについて学ぼう 実際にはScopeは文字列ではなく、オブジェク トになっていて、 Content()でも触れたように再度呼び出し用の 無名クラスが登録されているので、 その invoke() 関数を呼び出すことで、 もう一度呼び出せるようになります。

    これによって、変更の検知、変更されたときに呼び出 し直す必要があるスコープ。両方が取れるようになっ たのであとは呼び出すだけですね!
  85. Snapshot Systemとはなんなのか? multiversion concurrency control (MVCC)と呼ばれるもののようです。 https://ja.wikipedia.org/wiki/MultiVersion_Concurrency_Control > MultiVersion Concurrency

    Control (MVCC, マルチバージョン コンカレンシー コントロール) は、データ ベース管理システムの可用性を向上させる制御技術のひとつ。複数のユーザから同時に処理要求が行われ た場合でも同時並行性を失わずに処理し、かつ情報の一貫性を保証する仕組みが提供される。日本では多 版型同時実行制御、多重バージョン並行処理制御などと訳される。また単にマルチバージョンとも呼ばれる。 データベースでよく利用される技術のようです。 Snapshotクラスのapply()関数のコメントには以下の論文へのリンクもあります。 https://arxiv.org/pdf/1412.2324.pdf
  86. ステップ 4. Recompose

  87. ステップ 4. Recompose 0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする 1. Composable関数を呼び出して、情報を

    SlotTableに格納する 2. 3秒後のMutableStateへの変更 3. Snapshot Systemが変更をキャッチ 4. Recompose 5. GapBufferを使ったSlotTableへの反映 ビルド時 : 実行時: https://github.com/takahirom/inside-jetpack-compose-diagram
  88. ステップ 4. Recompose 先程のSnapshotの仕組みを使って、Composeは変更があった場所をただ呼び出して再構築するだけでしょ うか? Composeにはもっとすごい最適化が行われています。 どうやって変更がない部分を 再実行しないのか?? 前と同じデータになるところはスキップします。 これは

    donut-hole skipping と呼ばれる最適化で、穴が空いたように Content()は再実行(Recompose)され てもNode2()の中の処理は再実行 (Recompose)されません。
  89. Snapshotシステムとの連携によって、 このContent()が持つ無名クラスの invoke() 関数が呼び出される

  90. このラムダ内にはContent()を再実行するコー ドが書かれているので Content()をもう一度行 う。 つまり、Content()がRecomposeされる。

  91. Content()の再実行(Recompose)

  92. Node2()が呼び出される。 つまりNode2()もRecomposeされてしまうの か??

  93. ステップ 4. Recompose どうやって変更がない部分を呼び出さないのか?? Node2()が呼び出される。 つまりNode2()もRecomposeされてしまうの か??

  94. ステップ 4. Recompose どうやって変更がない部分を呼び出さないのか?? composer.change(引数)で呼び出すことで、 実際にComposable関数が呼び出された引数とSlotTableで保持してい るの今の場所にあるオブジェクトと比較 する。 引数が同じだったという結果を変数に入れる

  95. ステップ 4. Recompose どうやって変更がない部分を呼び出さないのか?? このif文ではtrueだとNode2にもともと書かれていた処理が スキップされ、 elseに入ると本来この関数に書かれていた処理が行われる。 ここでは引数がSlotTableの結果と同じだったので、 Node2()の関数自体は実行されているが、 Node2()に本来書かれていた処理はスキップになる

    。 (実際はちょっとデフォルト引数周りで正確ではないのですが、省略します ) 変更がない部分を 再実行されなくなった!
  96. ステップ 4. Recompose 変更があった部分に関しては SlotTableを変更して最新の状態に保つ必要が出てきます。 Recomposeでさ まざまな関数を呼び出しながら SlotTableの形を変えていくとちゃんと動くのか不安になりますよね? ComposeはRecompose中に変更があった部分を change

    listにためていって、最後に一気に反映します。
  97. ステップ 5. GapBufferを使ったSlotTableへの反映

  98. 0. Composable関数をCompose Kotlin Compiler PluginでSlotTableを作れるようにする 1. Composable関数を呼び出して、情報を SlotTableに格納する 2. 3秒後のMutableStateへの変更

    3. Snapshot Systemが変更をキャッチ 4. Recompose 5. GapBufferを使ったSlotTableへの反映 ビルド時 : 実行時: ステップ 5. GapBufferを使ったSlotTableへの反映 https://github.com/takahirom/inside-jetpack-compose-diagram
  99. ステップ 5. GapBufferを使ったSlotTableへの反映 仮想的なSlotTableを作ってNodeを消してみましょう。 右のようにNode2をNode1の位置に コピーすれば一瞬でできそうで、 シンプルで何も問題なさそうに見えます。 もし消すNodeが3つあったらどうな る??

  100. ステップ 5. GapBufferを使ったSlotTableへの反映 もしGap bufferなしで消すNodeが3つある場合。

  101. ステップ 5. GapBufferを使ったSlotTableへの反映 もしGap bufferなしで消すNodeが3つある場合。

  102. ステップ 5. GapBufferを使ったSlotTableへの反映 もしGap bufferなしで消すNodeが3つある場合。

  103. ステップ 5. GapBufferを使ったSlotTableへの反映 もしGap bufferなしだと、ずらしていく必要があるので、かなり時間がかかる この問題はノードの 追加でも同様に起こる しかも比較的単純な DroidKaigi公式アプリ でも

    371個このグループが あったりしてするので 重くなっちゃいそう
  104. ステップ 5. GapBufferを使ったSlotTableへの反映 Gap bufferとは? > A gap buffer in

    computer science is a dynamic array that allows efficient insertion and deletion operations clustered near the same location. Gap buffers are especially common in text editors, where most changes to the text occur at or near the current location of the cursor. https://en.wikipedia.org/wiki/Gap_buffer 同じ場所にたくさんのデータの挿入や削除がある場合に効率的に操作できる。 カーソルの近くで変更が多く起きるテキストエディタでよく用いられる。
  105. ステップ 5. GapBufferを使ったSlotTableへの反映 Gap bufferだとどうなる?

  106. ステップ 5. GapBufferを使ったSlotTableへの反映 Gap bufferと呼ばれるアルゴリズムだとどうなる? 変更前に 一度最後にデータを ずらす。 ここだけは一個ずつず らす必要がある。

  107. ステップ 5. GapBufferを使ったSlotTableへの反映 Gap bufferと呼ばれるアルゴリズムだとどうなる? プロパティで持っている Gap の終了indexをずらすだけ で、 Node1が削除できる!!

    Node2 Node3も同様🙌
  108. ステップ 5. GapBufferを使ったSlotTableへの反映 Gap bufferと呼ばれるアルゴリズムだとどうなる? これで削除完了

  109. ステップ 5. GapBufferを使ったSlotTableへの反映 Gap bufferと呼ばれるアルゴリズムだとどうなる? 最後にGapを戻す

  110. ステップ 5. GapBufferを使ったSlotTableへの反映 つまり今回の削除ではどうするのか?以下のようになる この削除のタイミングで 最初に話していた木の操作をする ApplierがNode1を消してくれます。 これで反映までできました! 🎉

  111. まとめ Kotlin Compiler Pluginによる Kotlin IRを利用したComposable関数への変更 Snapshot System(MVCC)を利用した MutableStateの購読場所の保持、 MutableStateの変更の検知

    Composable関数の再度呼び出し (Recompose) SlotTableとの比較による実行スキップ (Donut-hole skipping) Gap bufferのデータ構造、アルゴリズムを利用した SlotTableの更新 ビルド時 : 実行時: https://github.com/takahirom/inside-jetpack-compose-diagram Jetpack Composeはすごく面白い!!!
  112. 参考 Code Reading Materials https://qiita.com/takahirom/items/b29b7db652efe277498a https://qiita.com/takahirom/items/d2a89560f8ff2065a7c0 https://qiita.com/takahirom/items/0e72bee081de8cf4f05f https://qiita.com/takahirom/items/11e3ed72eb2f83440b12 https://qiita.com/takahirom/items/0e0a3559d95b49399c3b https://qiita.com/takahirom/items/8e978eeb6d85bf48a330

    https://qiita.com/takahirom/items/64bd9aa3278035671558 References CustomTreeCompositionSamples https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtim e/samples/src/main/java/androidx/compose/runtime/samples/CustomTreeCompositionSamples.kt Jetpack Compose Internals https://jorgecastillo.dev/book/ Under the hood of Jetpack Compose https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd Introduction to the Compose Snapshot system https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn What is “donut-hole skipping” in Jetpack Compose? https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose