普通に以下のセッションがめちゃくちゃわかりやすいのでそれを見ることをおすすめします
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は以下のような形になっています。
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があるはず。。?Greeting
やText
はUnitしかこれ返してないけど、どうやって追加してるの。。?
0.1.0-dev02で確認していきます
デバッグのミスなどで間違っている可能性などはかなりあるので、何かあればご指摘ください
コードを見ていく
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の追加のタイミングにデバッガーを仕掛けてみます。
このようにTextが追加されているのがわかります。このTextKt$Text$3$2$1$clidren...
、生成されている感があるので、気になります
このinstance
の値の出どころを探りましょう!
どうやらこのup()メソッドから渡されてくるようです。そして、_currentの値が渡されてくるようなので、down()
の呼び元を調べます。
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!") }
)
}
Greeting
やText
は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
することでラムダを埋め込んだコードを出してくれました。
まとめ
多分変更が何度も入っていくだろうとは思うので一瞬でこの知識は使えなくなるとは思いますが、ソースコードを追えるようになってきました。