Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Inside Jetpack Compose

takahirom
October 21, 2021

Inside Jetpack Compose

takahirom

October 21, 2021
Tweet

More Decks by takahirom

Other Decks in Programming

Transcript

  1. Inside Jetpack Compose
    takahirom

    View Slide

  2. 自分について 
    takahirom
    ● Takahiro Menju
    ● Androidに関する技術が好き
    ● DroidKaigi co-organizer
    ● Google Developers Expert for Android
    ● CyberAgent.Inc ABEMA
    ● Twitter takahirom (@new_runnable)
    ● GitHub takahirom

    View Slide

  3. コードが多いので
    ブログもご参照ください
    Inside Jetpack Compose
    https://qiita.com/takahirom/items/78e8ac1cf3
    82a0a79a9f
    English version:
    https://medium.com/@takahirom/inside-jetpa
    ck-compose-2e971675e55e

    View Slide

  4. なぜ内部を知るか?

    View Slide

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

    View Slide

  6. なぜ知るか?
    ● 中身を知りたくなったのが純粋な理由です。
    ● これからJetpack ComposeがAndroid開
    発のデファクトスタンダードになっていくの
    で、知っていると活躍できるかも?
    ● 調べて分かったのは Jetpack Composeの
    中身はかなり面白いので
    ぜひこのセッションを通して知ってほしい!
    https://d.android.com/jetpack/comp
    ose より

    View Slide

  7. どうやって内部を知るか?

    View Slide

  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
    より

    View Slide

  9. Jetpack Composeは
    Android以外でも使える
    WebやDesktopでも同じ仕組みを使っ
    ているプロダクトが存在する
    Compose for Web
    Compose for Desktop
    Mosaic
    (Compose for console UI)
    すごくシンプルな
    Jetpack Composeを使うツールを
    用意することで
    仕組みを知りやすくできるのでは?

    View Slide

  10. シンプルにJetpack Composeを使うコードの
    紹介
    https://github.com/takahirom/simple-compose-for-learning-inside-compose

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. シンプルに木構造をコンソールに出力する
    ● stateの変更をどうやって検知している
    の?🤔
    ● 変更があったときに再実行が必要な場
    所ってどうやって見つけているの? 🤔
    ● MutableStateを非同期で
    変更したら可変ステートだから危ないん
    じゃないの?🤔
    ● Node2()もう一回実行されちゃいそうだけ
    ど差分で実行されてないんじゃない? 🤔
    → あとで説明します
    まずはJetpack Composeで
    このコードを動かすための他の部品について説
    明します。
    https://github.com/takahirom/simple-compose-for-learning-inside-compose

    View Slide

  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

    View Slide

  23. シンプルに木構造をコンソールに出力する
    に必要なコードたち
    Nodeクラス
    ● 木構造を作るので、そのための Nodeク
    ラスを用意します。
    ● ただのシンプルなクラスで、何も Jetpack
    Composeのクラスなどを継承したりして
    いません。
    ● 子ノードへの参照(children)を持っていて
    木が作れるようになっています。
    https://github.com/takahirom/simple-compose-for-learning-inside-compose

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  35. ステップ0. Composable関数をCompose Kotlin
    Compiler PluginでSlotTableを作れるようにする
    Kotlin Compiler Pluginとは?
    Androidの開発でKotlinを使う場合は、
    KotlinをJavaバイトコードに変換し、その Javaバイトコー
    ドをdex形式に変換します。

    View Slide

  36. ステップ0. Composable関数をCompose Kotlin
    Compiler PluginでSlotTableを作れるようにする
    最近KotlinコンパイラではJavaバイトコードに変換する
    とき一度Kotlin IRと呼ばれる中間表現に変換するよう
    になりました。
    このKotlin IRをKotlin Compiler Pluginで書き換えるこ
    とができます。
    これによってJavaScriptなどにも変換できるのでマルチ
    プラットフォームにも対応できます。

    View Slide

  37. ステップ0. Composable関数をCompose Kotlin
    Compiler PluginでSlotTableを作れるようにする
    コンパイル後のJava Bytecodeをデコンパイルするこ
    とでCompose Compiler Pluginの書き換えを見るこ
    とができます。
    かんたんにこれができるライブラリも公開しています
    https://github.com/takahirom/decomposer

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  46. ステップ1. Composable関数を呼び出して、情報を
    SlotTableに格納する
    さて、ここでアプリが実際に起動して、先程の Content()などの関数が実行されます。
    今回は差分更新について詳しく見ていきたいのでここは詳しく説明しません。
    先程言ったとおり、Jetpack Composeは差分更新ができるので、差分を計算するための元
    の情報を持っておかないといけないですよね?
    そのための情報がComposable関数によってSlotTableというものに保存されます。

    View Slide

  47. 少しSlotTableについて知る

    View Slide

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

    View Slide

  49. SlotTableはIntArrayとAny型のArrayでどうやって動く
    のか??
    groups: IntArrayは5個一区切りで
    格納されている
    ● groups[0]からgroups[4]までが
    1つ目のグループ
    ● groups[5]からgroups[9]までが
    2つ目のグループ
    このようにするといい感じに中身を見られる
    どんな中身になっているか?

    View Slide

  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グループになっている

    View Slide

  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を持っているので
    木構造が作られている。

    View Slide

  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

    View Slide

  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: [email protected], androidx.compose.runtime.i
    Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: [email protected]]
    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)[email protected]: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: [email protected]]
    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: [email protected]]
    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というクラスに上記データが起動後に保持される

    View Slide

  54. ステップ 2. 3秒後のMutableStateへの変更
    ステップ 3. Snapshot Systemが変更をキャッチ

    View Slide

  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

    View Slide

  56. 3秒後、このstateをfalseに書き換えます
    このbyっていうのはどういうものでしょうか、
    remember{}とは?
    mutableStateOf(true) とは?
    ステップ 2. 3秒後のMutableStateへの変更
    ステップ 3. Snapshot Systemが変更をキャッチ

    View Slide

  57. ステップ 2. 3秒後のMutableStateへの変更
    ステップ 3. Snapshot Systemが変更をキャッチ
    `remember{}`はインスタンスの生存期間を伸ばしてくれ
    るもので、 `by` はKotlinのDelegated Propertyによるも
    ので、
    細かいところは違いますが、基本的には同じです。
    だいたい同じ
    LaunchedEffectによる別の場所からの MutableStateの変更を
    どのようにJetpack Composeがキャッチしているのか?

    View Slide

  58. Snapshot Systemについて学ぼう

    View Slide

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

    View Slide

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

    View Slide

  61. Snapshot Systemについて学ぼう
    フレーム間の変更をどのように検知するか?
    Snapshot.sendApplyNotifications()を追加すること
    でregisterApplyObserver()で渡しているapply
    observerが反応します
    出力

    View Slide

  62. Snapshot Systemについて学ぼう
    この仕組みを使って Jetpack Composeは
    フレームごとに溜まった変更を処理していきます。 LaunchedEffect{}内でのMutableStateへの変更もここ
    で見つかります。
    フレームの間はいいとして Composeが実際にRecompose中、
    つまりContent()などを呼んでいる時に別スレッドから MutableState
    が変更されたらどうなってしまうのか?
    実際のComposeのコードで変更を受け取るデバッグ画面
    フレームごとに溜まった変更を
    処理できそう!

    View Slide

  63. Snapshot Systemについて学ぼう
    別スレッドからの変更をどう解決するか?
    実は今までの例ではトップレベルに保持されている GlobalSnapshotを使っていましたが、 GlobalSnapshot
    内にSnapshotを作ることができます。
    ComposeはContent()などを呼び出しなおす
    Recomposeをする前に
    Snapshot.takeMutableSnapshot()でSnapshotを作
    成し、
    そのsnapshot内(Snapshot.enter{}内)で
    Recomposeします。Snapshotはゲームのセーブ
    ポイントだと思っていただければ大丈夫です。
    ちょっと実験してみよう

    View Slide

  64. 別スレッドからの変更をどう解決するか?
    MutableStateの書き換え

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  84. Snapshot Systemについて学ぼう
    実際にはScopeは文字列ではなく、オブジェク
    トになっていて、
    Content()でも触れたように再度呼び出し用の
    無名クラスが登録されているので、
    その invoke() 関数を呼び出すことで、
    もう一度呼び出せるようになります。
    これによって、変更の検知、変更されたときに呼び出
    し直す必要があるスコープ。両方が取れるようになっ
    たのであとは呼び出すだけですね!

    View Slide

  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

    View Slide

  86. ステップ 4. Recompose

    View Slide

  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

    View Slide

  88. ステップ 4. Recompose
    先程のSnapshotの仕組みを使って、Composeは変更があった場所をただ呼び出して再構築するだけでしょ
    うか? Composeにはもっとすごい最適化が行われています。
    どうやって変更がない部分を
    再実行しないのか??
    前と同じデータになるところはスキップします。
    これは donut-hole skipping と呼ばれる最適化で、穴が空いたように Content()は再実行(Recompose)され
    てもNode2()の中の処理は再実行 (Recompose)されません。

    View Slide

  89. Snapshotシステムとの連携によって、
    このContent()が持つ無名クラスの invoke()
    関数が呼び出される

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  95. ステップ 4. Recompose
    どうやって変更がない部分を呼び出さないのか??
    このif文ではtrueだとNode2にもともと書かれていた処理が
    スキップされ、
    elseに入ると本来この関数に書かれていた処理が行われる。
    ここでは引数がSlotTableの結果と同じだったので、
    Node2()の関数自体は実行されているが、
    Node2()に本来書かれていた処理はスキップになる 。
    (実際はちょっとデフォルト引数周りで正確ではないのですが、省略します )
    変更がない部分を
    再実行されなくなった!

    View Slide

  96. ステップ 4. Recompose
    変更があった部分に関しては SlotTableを変更して最新の状態に保つ必要が出てきます。 Recomposeでさ
    まざまな関数を呼び出しながら SlotTableの形を変えていくとちゃんと動くのか不安になりますよね?
    ComposeはRecompose中に変更があった部分を change listにためていって、最後に一気に反映します。

    View Slide

  97. ステップ 5. GapBufferを使ったSlotTableへの反映

    View Slide

  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

    View Slide

  99. ステップ 5. GapBufferを使ったSlotTableへの反映
    仮想的なSlotTableを作ってNodeを消してみましょう。
    右のようにNode2をNode1の位置に
    コピーすれば一瞬でできそうで、
    シンプルで何も問題なさそうに見えます。
    もし消すNodeが3つあったらどうな
    る??

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    同じ場所にたくさんのデータの挿入や削除がある場合に効率的に操作できる。
    カーソルの近くで変更が多く起きるテキストエディタでよく用いられる。

    View Slide

  105. ステップ 5. GapBufferを使ったSlotTableへの反映
    Gap bufferだとどうなる?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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はすごく面白い!!!

    View Slide

  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

    View Slide