9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Jetpack ComposeでHello WorldのLayoutNodeが追加されるまでを読んでみるメモ

Last updated at Posted at 2019-10-26

普通に以下のセッションがめちゃくちゃわかりやすいのでそれを見ることをおすすめします
Understanding Compose (Android Dev Summit '19)
https://www.youtube.com/watch?v=Q9MtlmmN4Q0

Understanding composeメモ
https://qiita.com/takahirom/items/e8fb7933fa44a546915f

モチベーション

Jetpack ComposeのHello Worldは以下のような形になっています。
image.png

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

この@Composeで何が起こるのか。。? 実際には馴染みのAndroidのViewがあるはず。。?GreetingTextはUnitしかこれ返してないけど、どうやって追加してるの。。? :thinking:

0.1.0-dev02で確認していきます

デバッグのミスなどで間違っている可能性などはかなりあるので、何かあればご指摘ください :pray:

コードを見ていく

LayoutNodeとは?

実際には馴染みのAndroidのViewがあるはず。。?

setContentの中は以下のようになっており、ComposeはAndroidComposeViewというViewに描画するようです。

fun Activity.setContent(
    content: @Composable() () -> Unit
): CompositionContext? {
    val composeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? AndroidComposeView
        ?: AndroidComposeView(this).also { setContentView(it) }  // **← ここでnewする!!**

AndroidComposeViewは以下のようにrootのレイアウトを持っています。

class AndroidComposeView constructor(context: Context) :
    ViewGroup(context), Owner, SemanticsTreeProvider, DensityScope {
    override var density: Density = Density(context)
        private set

    val root = LayoutNode().also { // **← rootのLayoutNode!!**
        it.measureBlocks = RootMeasureBlocks
    }

このrootのNodeにTextが追加されるようです。
ちなみにこのLayoutNodeクラスはComponentNodeのサブクラスになっており、ComponentNodeは以下のように子codeを持ちます。

sealed class ComponentNode : Emittable {
    internal val children = mutableListOf<ComponentNode>()  // **← childrenを保持する!!**

TextのLayoutNodeを追加しているのは誰か?

このchildrenの追加のタイミングにデバッガーを仕掛けてみます。
image.png
このようにTextが追加されているのがわかります。このTextKt$Text$3$2$1$clidren...、生成されている感があるので、気になります
このinstanceの値の出どころを探りましょう!

どうやらこのup()メソッドから渡されてくるようです。そして、_currentの値が渡されてくるようなので、down()の呼び元を調べます。
image.png

image.png

down()の呼び元としてemitNode()があるのですが、、emitNode()を呼び出しているところがスタックトレースでは追えない部分が出てきます。

コンパイルされたコードの中から来るLayoutNode

コードを見ていくと実際にはViewComposer内の以下のような実装が適応されているようです。

    inline fun <T : View> emit(
        key: Any,
        /*crossinline*/
        ctor: (context: Context) -> T,
        update: ViewUpdater<T>.() -> Unit
    ) = with(composer) {
        startNode(key)
        // **ここにemitNodeがある**
        val node = if (inserting) ctor(context).also { emitNode(it) } 
        else useNode() as T
        ViewUpdater<T>(this, node).update()
        endNode()
    }
...
    @Suppress("PLUGIN_WARNING")
    inline fun call(
        key: Any,
        /*crossinline*/
        invalid: ViewValidator.() -> Boolean,
        block: @Composable() () -> Unit
    ) = with(composer) {
        startGroup(key)
        if (ViewValidator(composer).invalid() || inserting) {
            startGroup(invocation)
            block()
            endGroup()
        } else {
            skipCurrentGroup()
        }
        endGroup()
    }

そこでコンパイラのコードをちょっと見ると以下のようなコメントを発見できます。
https://android.googlesource.com/platform/frameworks/support/+/a5396b43ef905756b18805fe0ae31a99e96e6df6/compose/compose-compiler-hosted/src/main/java/androidx/compose/plugins/kotlin/compiler/lower/ComposableCallTransformer.kt#239

つまり**Foo(text="foo")composer.call()に変換しています**

        /*
        Foo(text="foo")
        // transforms into
        val attr_text = "foo"
        composer.call(
            key = 123,
            invalid = { changed(attr_text) },
            block = { Foo(attr_text) }
        )
         */

つまり最初のMainActivityのコードは @Composeをなくして以下のように書くことができます。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting("Android")
            }
        }
    }
}

//@Composable
//fun Greeting(name: String) {
//    Text(text = "Hello $name!")
//}

fun Greeting(name: String) {
    composer.call(
        key = 0x9a8fdf7d,
        invalid = { changed(name) },
        block = { Text(text = "Hello $name!") }
    )
}

GreetingTextはUnitしかこれ返してないけど、どうやって追加してるの。。?

これと同じことがText()の中でも行われ、そこでcomposable.emit()に変換され、emitNode()を呼んでいるようです。

デバッグ方法のメモ

公開されているコードなのですが、かなりinline化されていてデバッガーで追いにくいです。またAndroid StudioについているKotlinのJavaへのデコンパイラではラムダを埋め込んでくれないので実質追えません。
brew cask install jadなどでjadをインストールして
./gradlew compileDebugKotlin
などをした後に

cd app/build/tmp/kotlin-classes/debug/[パッケージ]
jad *.class

することでラムダを埋め込んだコードを出してくれました。

まとめ

多分変更が何度も入っていくだろうとは思うので一瞬でこの知識は使えなくなるとは思いますが、ソースコードを追えるようになってきました。

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?