Slide 1

Slide 1 text

Inside Jetpack Compose takahirom

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

なぜ内部を知るか?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

普通に見るかなり複雑 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 より

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

シンプルに木構造をコンソールに出力するのに必要な コードリスト この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

Slide 13

Slide 13 text

シンプルに木構造をコンソールに出力するのに必要な コードリスト この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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

この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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

この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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

この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

Slide 27

Slide 27 text

シンプルに木構造をコンソールに出力する に必要なコードたち 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

Slide 28

Slide 28 text

この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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

ステップを追って見ていこう 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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

ステップ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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

ステップ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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

少しSlotTableについて知る

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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というクラスに上記データが起動後に保持される

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

Snapshot Systemについて学ぼう

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

ステップ 4. Recompose

Slide 87

Slide 87 text

ステップ 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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Content()の再実行(Recompose)

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

参考 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