Jetpack Composeの仕組みを使ってAndroid FrameworkのViewを作ってみて仕組みを知る


Jetpack ComposeはTextViewやLinearLayoutなどといったAndroid FrameworkのViewを使ってコンポーネントを描画するわけではなく、新しく作ったComponentをCanvasにdrawしています。
Jetpack ComposeはAndroidのUIの部分と木構造を扱う部分(Tree diff toolとして利用できる)としてのComposeがあり、その木構造を扱う部分の仕組みを使うことで、別のUIも作れてしまいます。 (詳しくは https://github.com/JakeWharton/mosaic/#i-thought-jetpack-compose-was-a-ui-toolkit-for-android )

例えばJetpack Compose for Web、JakeWharton/mosaicなどです。

そのため、Android FrameworkのViewを置けるようにもできます。

理解を深めるために、こんな感じのLinearLayoutとTextViewだけ置けるサンプルを作ってみました。

コード

@Composable
private fun AndroidViewApp() {
    var count by remember { mutableStateOf(1) }
    LinearLayout {
        TextView(
            text = "This is the Android TextView!!",
        )
        repeat(count) {
            TextView(
                text = "Android View!!TextView:$it $count",
                onClick = {
                    count++
                }
            )
        }
    }
}

作って満足していたんですが、Composeでなにかしたくなったときに、定期的にこの仕組みを思い出したくなるので、書いておきます。

もう少しシンプルにして以下のようにします。

@Composable
private fun AndroidViewApp() {
    LinearLayout {
        TextView(
            text = "This is the Android TextView!!",
        )
    }
}

LinearLayout()TextView()は独自に定義したComposable関数です。

それを元の関数に展開したり、ちょっと関連が薄い部分を外すと以下になります。
以下のようにその要素を作る factory = {}と、要素をアップデートできるupdate = {}があります。

@Composable
private fun AndroidViewApp(context: Context) {
    ComposeNode<LinearLayout, ViewApplier>(
        factory = {
            LinearLayout(context)
        },
        update = {},
        content = {
            val text = "This is the Android TextView!!"
            ComposeNode<TextView, ViewApplier>(
                factory = {
                    TextView(context)
                },
                update = {
                    set(text) {
                        this.text = text
                    }
                },
            )
        }
    )
}

ここに書いてあることで、それぞれのViewの作り方とアップデートの仕方はわかりました。
しかしどのように親Viewと子Viewを結びつけるのでしょうか?
それは以下のViewApplierにて実装しています。
このViewApplierが使われることによって親子関係が設定されることで利用できます。

class ViewApplier(val view: FrameLayout) : AbstractApplier<View>(view) {
    override fun onClear() {
        (view as? ViewGroup)?.removeAllViews()
    }

    override fun insertBottomUp(index: Int, instance: View) {
        (current as? ViewGroup)?.addView(instance, index)
    }

    override fun insertTopDown(index: Int, instance: View) {
    }

    override fun move(from: Int, to: Int, count: Int) {
        // NOT Supported
    }

    override fun remove(index: Int, count: Int) {
        (view as? ViewGroup)?.removeViews(index, count)
    }
}

このViewApplierは最初にCompositionというインスタンスを作るときに必要になり、これで親子関係が作られるようです。
Recomposerはいろいろ処理するところのようで、スタックトレースを見るとここからApplierやComposeNodeが呼ばれるようです。

    val composer = Recomposer(Dispatchers.Main)

    // composerの初期化処理

    val rootView = FrameLayout(context)
    Composition(ViewApplier(rootView), composer).setContent {
        AndroidViewApp(context)
    }

まとめ

ComposeはUIの部分と木構造を扱う部分からできている。
Applierが木構造を操作する。
ComposeNodeがそれぞれのノードの作成とアップデートを行う。

まとめると以下のようになります。