$30 off During Our Annual Pro Sale. View Details »

[DroidKaigi 2023] Modifier.Node を使いましょう!

すーじ
September 13, 2023

[DroidKaigi 2023] Modifier.Node を使いましょう!

DroidKaigi 2023 で「Modifier.Node を使いましょう!」セッションの発表資料です。
http://2023.droidkaigi.jp/timetable/492313/

すーじ

September 13, 2023
Tweet

More Decks by すーじ

Other Decks in Education

Transcript

  1. Modifier.Nodeを使いましょう
    DroidKaigi 2023

    View Slide

  2. @_SUR4J_
    Suraj (すーじ)
    株式会社サイバーエージェント
    株式会社AbemaTV
    ABEMA の AndroidTV アプリ開発。

    View Slide

  3. Modifier.composed のパフォーマンス問題
    Modifier.Node の紹介
    既存の Modifier.composed を移行する
    目次

    View Slide

  4. Modifier.composed のパフォーマンス問題

    View Slide

  5. Runtime の Smart Recomposition
    Modifier.composed のパフォーマンス問題

    View Slide

  6. @Composable
    fun Sample(){
    var x by remember {..}
    Text("Hi")
    Row{
    Text("App")
    Image(..)
    }
    }
    「Composition」は @Composable 関数が
    ノードになっているツリーです。
    Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    Runtime の Smart Recomposition

    View Slide

  7. Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    @Composable
    fun Sample(){
    var x by remember {*.}
    Text("Hi")
    Row{
    Text("App")
    Image(*.)
    }
    }
    Runtime の Smart Recomposition

    View Slide

  8. Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    @Composable
    fun Sample(){
    var x by remember {*.}
    Text("Hi")
    Row{
    Text("App")
    Image(*.)
    }
    }
    Runtime の Smart Recomposition

    View Slide

  9. Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    Hi
    @Composable
    fun Sample(){
    var x by remember {*.}
    Text("Hi")
    Row{
    Text("App")
    Image(*.)
    }
    }
    Runtime の Smart Recomposition

    View Slide

  10. Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    Hi
    @Composable
    fun Sample(){
    var x by remember {*.}
    Text("Hi")
    Row{
    Text("App")
    Image(*.)
    }
    }
    Runtime の Smart Recomposition

    View Slide

  11. @Composable
    fun Sample(y: String){
    var x by remember {*.}
    Text("Hi")
    Row{
    Text("App")
    Image(*.)
    }
    }
    App
    Hi
    Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    Runtime の Smart Recomposition

    View Slide

  12. @Composable
    fun Sample(y: String){
    var x by remember {*.}
    Text("Hi")
    Row{
    Text("App")
    Image(*.)
    }
    }
    App
    Hi
    Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    Runtime の Smart Recomposition

    View Slide

  13. 毎回ツリーの全てのノードを訪れるより、
    Runtime が Composition ツリー全体を観察し、
    変更が行った @Composable関数のみを再呼び出しします。
    毎回ツリーの全てのノードを訪れるより、
    Runtime が Composition ツリー全体を観察し、
    毎回ツリーの全てのノードを訪れるより、
    Runtime の Smart Recomposition

    View Slide

  14. @Composable
    fun Sample(y: String){
    var x by remember {*.}
    Text("Hi")
    Row{
    Text("App")
    Image(*.)
    }
    }
    Text("Hello")
    App
    Hi
    Runtime の Smart Recomposition
    Row
    Sample
    Text(App)
    remember
    Image
    Text(Hi)

    View Slide

  15. @Composable
    fun Sample(y: String){
    var x by remember {*.}
    Text("Hello")
    Row{
    Text("App")
    Image(*.)
    }
    }
    App
    Hello
    Row
    Text(Hi)
    Sample
    Text(App)
    remember
    Image
    Smart Recomposition の軽く紹介

    View Slide

  16. 「変更が行った」と判断された @Composable のみが選択的な再呼び出され、
    他の @Composable の呼び出しスキップさせることを
    一般的にスマートな Recomposition と呼ばれてます。
    Runtime の Smart Recomposition

    View Slide

  17. Smart Recomposition の条件
    Modifier.composed のパフォーマンス問題

    View Slide

  18. Unit を返す @Composable のパラメーターが
    変更されなかったらスキップされます。
    Smart Recomposition の条件

    View Slide

  19. @Composable fun Text(text: String, *.)
    Text(text = "Hi") → Text(text = "Hi")
    Text(text = "Hi") → Text(text = "Hello")
    スキップされます
    スキップされない
    Smart Recomposition の条件

    View Slide

  20. Unit 以外の値を返す @Composable を
    スキップできない。
    Smart Recomposition の条件

    View Slide

  21. @Composable
    fun Button(*.)
    @Composable
    fun rememberLazyListState(): LazyListState
    スキップすることが可能
    スキップされない
    Smart Recomposition の条件

    View Slide

  22. 旧 Modifier 仕組み
    Modifier.composed のパフォーマンス問題

    View Slide

  23. Modifier.background(color: Color, *.)
    = this.then(Background(color, ..))
    Background
    旧 Modifier 仕組み
    Modifier はそれぞれの Modifier.Element をオブジェクト
    を発行しました。

    View Slide

  24. Modifier.background(color: Color, *.)
    .padding(start: Dp, *.)
    = this.then(Background(color, ..))
    .then(PaddingElement(start, ..))
    PaddingElement
    Background
    複数の場合、発行されたそれぞれの Modifier.Element が
    順次的に連結されました。
    旧 Modifier 仕組み

    View Slide

  25. Modifier チェーンの 2つの役割がありました。
    旧 Modifier 仕組み

    View Slide

  26. Background(Red)
    PointerInputElement
    PaddingElement(16)
    drawRect(color = Red)
    onPointerEvent { .. }
    it.placeRelative(x = 16, ..)
    Red 色の背後
    タップイベントを対応
    コンテンツ周り空白
    Box
    旧 Modifier 仕組み

    View Slide

  27. Modifier.Element のチェーンがレイアウトに
    それぞれの特定なプロパティを提供しました。
    役割 #1
    旧 Modifier 仕組み

    View Slide

  28. Box(modifier = Modifier
    .background(Color.Blue)
    ..
    .padding(16.dp)
    )
    Color.Red
    旧 Modifier 仕組み

    View Slide

  29. Box
    =
    =

    . . .
    Background(Red)
    PaddingElement
    . . .
    Background(Blue)
    PaddingElement
    旧 Modifier 仕組み

    View Slide

  30. Smart Recomposition のため
    Modifier のチェーンが比較されました。
    役割 #2
    旧 Modifier 仕組み

    View Slide

  31. Modifier.composed
    Modifier.composed のパフォーマンス問題
    Modifier.composed のパフォーマンス問題

    View Slide

  32. 毎回新しいインスタンス生成されましたので、
    レイアウト毎のstate を保持することができなかったです。
    毎回新しいインスタンス生成されましたので、
    Modifier.composed が追加された理由
    Modifier.background(*.) = this.then(Background(..))
    class Background (val color: Color..): Modifier.Element {
    ** *. */
    }

    View Slide

  33. Modifier.composed(
    factory: @Composable () *> Modifier, *.
    ) = this.then(ComposedModifier(factory, *.))
    Modifier に state を持たせるため、
    Modifier.composed が追加されました。
    Modifier.composed が追加された理由

    View Slide

  34. fun Modifier.clickable(*.) = this.composed {
    var enabled by remember{ mutableStateOf(false) }
    val clickableState by remember{ ClickableState() }
    return Modifier**.onPointerEvent{ ** *. */ }
    }
    Compose Runtime を活用して
    レイアウト毎のstate を保持することができました。
    Modifier.composed が追加された理由

    View Slide

  35. Modifier.composed の2つの問題が発生しました。
    Modifier.composed が追加された理由

    View Slide

  36. Modifier.composed を比較できない
    Modifier.composed のパフォーマンス問題

    View Slide

  37. @Composable
    fun Box(*., content: @Composable () .> Unit)
    キャッシュされます
    Modifier.composed を比較できない
    Compose Runtime が @Composable 関数に渡される
    @Composable ラムダをキャッシュします。

    View Slide

  38. fun Modifier.composed(
    factory: @Composable () .> Modifier
    ): Modifier { ** *. */ }
    Modifier.composed は @Composable 関数に渡される
    @Composable ラムダをキャッシュします。
    キャッシュされない
    Modifier.composed を比較できない

    View Slide

  39. Box(modifier = Modifier
    .background(Color.Red)
    .composed{ /*..*/ }
    .padding(16.dp)
    )
    Modifier.composed を比較できない

    View Slide

  40. background (Red)
    composed(Factory@xyz456)
    padding (16.dp)
    =
    =

    Box
    Background (Red)
    composed(Factory@abc123)
    PaddingElement(16.dp)
    background (Red)
    composed(Factory@abc123)
    padding (16.dp)
    composed(Factory@xyz456)
    Background (Red)
    PaddingElement(16.dp)
    background (Red)
    composed(Factory@abc123)
    padding (16.dp)
    composed(Factory@abc123)
    Modifier.composed を比較できない

    View Slide

  41. Modifier.composed を比較できないので
    変更がなくても不必要な Recomposition 発生されました。
    問題 #1
    Modifier.composed を比較できない

    View Slide

  42. Modifier.composed で Compositionツリーが大きく
    なった
    Modifier.composed のパフォーマンス問題

    View Slide

  43. fun Modifier.composed(
    factory: @Composable () .> Modifier
    ): Modifier { ** *. */ }
    Compositionツリーが大きくなった
    値を返す @Composable なので、Smart Recomposition によって
    呼び出しをスキップできない。

    View Slide

  44. fun Modifier.clickable() = composed {
    var enabled by remember{ mutableStateOf(*.) }
    val state = remember{ ClickableState() }
    LaunchedEffect{ ** *. */ }
    ** *. */
    }
    @Composable 関数が Composition ツリーに追加され、
    Runtime の観察するコストが増えました。
    Compositionツリーが大きくなった

    View Slide

  45. Compose Modifiers deep dive | Android Dev Summit’22
    https://youtu.be/BjGX2RftXsU?si=uHkVADOwnWCkFYmf&t=713
    Compositionツリーが大きくなった

    View Slide

  46. Box
    Box
    34 remember + 11 Side effects..
    Box(modifier = Modifier..clickable{..})
    Compositionツリーが大きくなった

    View Slide

  47. Composition ツリーのサイズが増えて、
    Runtime の観察コストが大きくなりました。
    Composition ツリーのサイズが増えて、
    Runtime の観察コストが大きくなりました。
    問題 #2
    Compositionツリーが大きくなった

    View Slide

  48. LazyList{
    items(list.size) {
    ListItem(modifier = Modifier.clickable{}, *.)
    }
    ** *. */
    }
    Compositionツリーが大きくなった

    View Slide

  49. Box
    Box Box Box
    Box
    Box
    Box Box Box
    Box
    Compositionツリーが大きくなった

    View Slide

  50. Enter QR HERE
    最初のCLのリンク
    Modifier.composed のパフォーマンス問題

    View Slide

  51. Enter QR HERE
    動画リンク
    Modifier.composed のパフォーマンス問題

    View Slide

  52. Modifier チェーンの役割
    ● レイアウトにそれぞれの特定なプロパティを提供。
    ● 「Modifier が変更された」の判断のため比較。
    Modifier に state を持たせるために Modifier.composed が追加されました。
    Modifier.composed で Compose Runtime のパフォーマンスが影響されました。
    Modifier.composed のパフォーマンス問題
    Modifier チェーンの役割
    ● レイアウトにそれぞれの特定なプロパティを提供。
    ● 「Modifier が変更された」の判断のため比較。
    Modifier に state を持たせるために Modifier.composed が追加されました。
    まとめ
    Modifier チェーンの役割
    ● レイアウトにそれぞれの特定なプロパティを提供。
    ● 「Modifier が変更された」の判断のため比較。

    View Slide

  53. Modifier.Node の紹介

    View Slide

  54. 2つのチェーン
    Modifier.Node の紹介

    View Slide

  55. Box
    BackgroundElement
    PointerInputElement
    PaddingElement
    BackgroundNode
    PointerInputNode
    PaddingNode
    Element チェーン Node チェーン
    2つのチェーン
    新しい Modifier 仕組みで
    旧 Modifier チェーンが2つのチェーンに分けられました。

    View Slide

  56. ModifierNodeElement チェーン
    BackgroundElement
    PointerInputElement
    PaddingElement
    BackgroundNode
    PointerInputNode
    PaddingNode
    Box
    レイアウトにそれぞれの特定なプロパ
    ティを提供します。
    レイアウトが Node チェーンの1つのイン
    スタンスを持ち続けます。
    Node チェーン
    2つのチェーン

    View Slide

  57. Node チェーン
    BackgroundElement
    PointerInputElement
    PaddingElement
    Box
    Element チェーン
    BackgroundNode
    PointerInputNode
    PaddingNode
    「Modifier が変更された」の判断のため
    に比較されます。
    毎回 Element 新しいインスタンスが生
    成されます。
    2つのチェーン

    View Slide

  58. Modifier
    .background(Color.Blue)
    .padding(16.dp)
    Modifier
    .then(BackgroundElement(Color.Blue))
    .then(PaddingElement(16.dp))
    PaddingElement
    BackgroundElement
    2つのチェーン

    View Slide

  59. Box(modifier = Modifier
    .then(BackgroundElement(Color.Blue))
    .then(PaddingElement(16.dp))
    )
    2つのチェーン
    Modifier
    .then(BackgroundElement(Color.Blue))
    .then(PaddingElement(16.dp))

    View Slide

  60. Box
    PaddingElement
    BackgroundElement
    PaddingNode
    BackgroundNode
    2つのチェーン

    View Slide

  61. Modifier.Node
    Modifier.Node の紹介

    View Slide

  62. ● レイアウトにそれぞれの特定なプロパティーを提供します。
    ● レイアウトと同じ寿命を持ちますので、state を保持することができます。
    Modifier.Node
    interface Modifier {
    abstract class Node { ** *. */ }
    interface Element { ** *. */ }
    }

    View Slide

  63. interface Modifier {
    abstract class Node {
    open fun onAttach() {}
    open fun onDetach() {}
    var isAttached: Boolean
    private set
    ** *. */
    }
    }
    Modifier.Node がチェーンに追加さ
    れる直後に呼び出されます。
    Modifier.Node

    View Slide

  64. Box(modifier = Modifier
    .background(Color.Blue)
    .clickable{ .* .. ./ }
    .padding(16.dp))
    Box(modifier = Modifier
    .background(Color.Blue)
    .padding(16.dp))
    Modifier.Node

    View Slide

  65. PaddingNode
    onAttach()
    ClickableNode
    PaddingNode
    BackgroundNode
    Box
    PaddingNode
    BackgroundNode
    Box
    Modifier.Node

    View Slide

  66. interface Modifier {
    abstract class Node {
    open fun onAttach() {}
    open fun onDetach() {}
    var isAttached: Boolean
    private set
    ** *. */
    }
    }
    Modifier.Node チェーンから外れた
    直前に呼び出されます。
    Modifier.Node

    View Slide

  67. Box(modifier = Modifier
    .background(Color.Blue)
    .clickable{ ** *. */ }
    .padding(16.dp))
    Box(modifier = Modifier
    .background(Color.Blue)
    ./.clickable{ .* .. ./ }
    .padding(16.dp))
    Modifier.Node

    View Slide

  68. PaddingNode
    onDetach()
    ClickableNode PaddingNode
    BackgroundNode
    Box
    ClickableNode
    BackgroundNode
    Box
    Modifier.Node

    View Slide

  69. interface Modifier {
    abstract class Node {
    open fun onAttach() {}
    open fun onDetach() {}
    var isAttached: Boolean
    private set
    ** *. */
    }
    }
    いくつかの API が onAttach と
    onDetach の間でしか呼び出されな
    い。
    isAttached が onAttach と
    onDetach の間で true になってま
    す。
    いくつかの API が onAttach と
    onDetach の間でしか呼び出されな
    い。
    Modifier.Node

    View Slide

  70. ModifierNodeElement
    Modifier.Node の紹介

    View Slide

  71. abstract class ModifierNodeElement
    : Modifier.Element { ** *. */ }
    ModifierNodeElement
    ● レイアウトを直接的に影響しないです。
    ● 「Modifier が変更された」の判断のために比較されます。

    View Slide

  72. abstract class ModifierNodeElement {
    abstract fun create(): N
    abstract fun update(node: N)
    abstract fun equals(other: Any?)
    abstract fun hashCode(): Int
    ** *. */
    }
    Modifier がはじめてレイアウトに設定された時呼び出されます。
    新しい Modifier.Node のインスタンスを返します。
    ModifierNodeElement

    View Slide

  73. Box(modifier = Modifier
    .background(Color.Blue)
    ..
    )
    Box(modifier = Modifier
    .background(Color.Blue)
    ..
    .clickable{ .* .. ./ }
    )
    ModifierNodeElement

    View Slide

  74. Background..(Blue)
    Box
    Background..(Blue)
    ClickableElement
    ClickableNode
    BackgroundNode
    create()
    Background..(Blue)
    ClickableElement
    ModifierNodeElement

    View Slide

  75. Modifier が変更された時呼び出されます。
    既存の Modifier.Node で保持されてる state を更新するチャンスです。
    Modifier が変更された時呼び出されます。
    abstract class ModifierNodeElement {
    abstract fun create(): N
    abstract fun update(node: N)
    abstract fun equals(other: Any?)
    abstract fun hashCode(): Int
    ** *. */
    }
    ModifierNodeElement

    View Slide

  76. Color.Red
    Box(modifier = Modifier
    .background(Color.Blue)
    .padding(16.dp)
    ..
    )
    ModifierNodeElement

    View Slide

  77. PaddingNode
    BackgroundNode
    Box
    PaddingElement
    Background..(Red)
    update()
    PaddingElement
    Background..(Blue)
    BackgroundNode
    Background..(Red)
    ModifierNodeElement

    View Slide

  78. abstract class ModifierNodeElement {
    abstract fun create(): N
    abstract fun update(node: N)
    abstract fun equals(other: Any?)
    abstract fun hashCode(): Int
    }
    このメソッドで update を呼び出すかどうかの判断行われます。
    ModifierNodeElement

    View Slide

  79. Color.Red
    Box(modifier = Modifier
    .background(Color.Blue)
    .padding(16.dp)
    ..
    )
    ModifierNodeElement

    View Slide

  80. equals()
    PaddingElement
    BackgroundElement
    update()
    PaddingElement
    BackgroundElement
    equals() false
    true
    PaddingElement
    BackgroundElement
    ModifierNodeElement

    View Slide

  81. Enter QR HERE
    記事リンク
    ModifierNodeElement

    View Slide

  82. 既存の Modifier.composed を移行する

    View Slide

  83. 基本手順
    既存の Modifier.composed を移行する

    View Slide

  84. ModifierNodeElement、Modifier.Nodeクラス
    の作成
    基本手順

    View Slide

  85. private class FocusableNode(..)
    : Modifier.Node() { ** *. */ }
    fun Modifier.focusable(
    interactionSource: MutableInteractionSource? = null
    ) = composed { ** *. */ }
    private class FocusableElement(..)
    : ModifierNodeElement{ ** *. * / }
    ModifierNodeElement、Modifier.Nodeクラスの作成

    View Slide

  86. private class FocusableElement(
    val interactionSource: MutableInteractionSource?
    ) : ModifierNodeElement{ ** *. * / }
    fun Modifier.focusable(
    interactionSource: MutableInteractionSource? = null
    ) = composed { ** *. */ }
    Modifier 関数のパラメータを、ModifierNodeElement クラスの
    コンストラクタパラメータとして設定します。
    ModifierNodeElement、Modifier.Nodeクラスの作成

    View Slide

  87. fun Modifier.focusable(
    interactionSource: MutableInteractionSource? = null
    ) = this.then(FocusableElement(interactionSource))
    Modifier 関数自体で Modifier.then に
    ModifierNodeElement のインスタンスを渡します。
    ModifierNodeElement、Modifier.Nodeクラスの作成

    View Slide

  88. Modifier.Node の実装
    Modifier.Node 作成の基本

    View Slide

  89. Modifier.Node の実装
    Modifier.Node はレイアウトにプロパティを提供する役割持ってるので、
     Modifier.composed の内部実装を複製するような実装を行われます。

    View Slide

  90. fun Modifier.focusable(*.) = composed {
    var isFocused = remember { mutableStateOf(false) }
    DisposableEffect(isFocused) { .* .. ./ }
    Modifier*.
    }
    @Composable コンテキストが
    ないと呼び出せない
    Modifier.Node の実装

    View Slide

  91. fun Modifier.focusable(*.) = composed {
    var isFocused = remember { mutableStateOf(false) }
    DisposableEffect(isFocused) { ** *. */ }
    Modifier
    .semantics { .* .. ./ }
    .then(FocusedBoundsModifier())
    .onFocusChanged { .* .. ./ }
    }
    Modifier.Node の実装

    View Slide

  92. private class FocusChangedNode(*.) : FocusEventModifierNode {
    override fun onFocusEvent(focusState: FocusState) {
    ** *. */
    }
    }
    Modifier.Node の実装
    Modifier.onFocusChanged の Modifier.Node は
    FocusEventModifierNode に拡張され、onFocusEvent で実装が行われてます。

    View Slide

  93. private class FocusableNode(*.)
    : FocusEventModifierNode, Modifier.Node() {
    override fun onFocusEvent(focusState: FocusState) {
    ** *. */
    }
    }
    Modifier.Node の実装
    同様で新しい Modifier.Node を
    FocusEventModifierNode に拡張され、onFocusEvent で実装が行われてます。

    View Slide

  94. fun Modifier.focusable(*.) = composed {
    Modifier.onFocusChanged { state *> isFocused = state.isFocused }
    }
    Modifier.Node の実装
    private class FocusChangedNode(var onFocusChanged: (FocusState) .> Unit)
    : FocusEventModifierNode {
    override fun onFocusEvent(focusState: FocusState) {
    onFocusChanged.invoke(focusState)
    }
    }

    View Slide

  95. Modifier.Node の実装
    fun Modifier.focusable(*.) = composed {
    ** *. */
    Modifier
    .onFocusChanged { state .> isFocused = state.isFocused }
    }
    private class FocusableNode(*.) : SemanticsModifierNode, *. {
    override fun onFocusEvent(state: FocusState) {
    this.isFocused = state.isFocused
    }
    }
    onFocusEvent で Modifier.onFocusChanged に渡されたラムダ
    の実装が行われます。

    View Slide

  96. Enter QR HERE
    記事リンク
    Modifier.Node の実装
    FocusEventModifierNode を含めていくつかの
    Modifier.Node interface をこの記事で紹介してます。

    View Slide

  97. ModifierNodeElement の実装
    基本手順

    View Slide

  98. ModifierNodeElement の実装
    private class FocusableElement(
    val interactionSource : MutableInteractionSource?
    ) : ModifierNodeElement {
    override fun create(): FocusableNode
    = FocusableNode(this.interactionSource)
    ** *. */
    }

    View Slide

  99. private class FocusableElement(
    val interactionSource : MutableInteractionSource?
    ) : ModifierNodeElement {
    ** *. */
    override fun update(node: FocusableNode) {
    node.interactionSource = this.interactionSource
    }
    }
    ModifierNodeElement の実装

    View Slide

  100. private class FocusableElement(
    val interactionSource : MutableInteractionSource?
    ) : ModifierNodeElement {
    ** *. */
    override fun equals(other: Any?); Boolean { .* .. ./ }
    override fun hashCode(): Int { .* .. ./ }
    }
    ModifierNodeElement の実装

    View Slide

  101. private class FocusableElement(
    val interactionSource : MutableInteractionSource?
    ) : ModifierNodeElement {
    ** *. */
    override fun equals(other: Any?): Boolean {
    return other is FocusableElement .&
    other.interactionSource
    .= this.interactionSource
    }
    }
    ModifierNodeElement の実装

    View Slide

  102. 基本手順
    ● Modifier.Node と ModifierNodeElement クラスを作成する。
    ● Modifier の内部実装と組み合わて作られてる Modifier の 内部実装を複製する
    ような実装を Modifier.Node で行う。
    ● ModifierNodeElement の create で Modifier.Node の作成、update で
    Modifier.Node の state を更新とModifier が比較される判断実装を行う。
    ● Modifier.composed の代わりに Modifier.then 使って、
    ModifierNodeElement のインスタンスを渡す。
    まとめ

    View Slide

  103. Modifier.Node に移行するチップス
    Modifier.Node 作成の基本

    View Slide

  104. Enter QR HERE
    Modifier.Node 移行
    の CL のリンク
    Modifier.Node に書き換えるチップス

    View Slide

  105. Official best practices in plans
    公式ガイドラインが作成中
    Modifier.Node に移行するチップス

    View Slide

  106. Enter QR HERE
    記事のリンク
    Modifier.Node に移行するチップス

    View Slide

  107. remember{..}
    Modifier.Node に書き換えるチップス

    View Slide

  108. fun Modifier.focusable(*.) = composed {
    val bringIntoViewRequester
    = remember { BringIntoViewRequester() }
    }
    class FocusableNode : Modifier.Node, *. {
    private val bringIntoViewRequester
    = BringIntoViewRequester()
    }
    remember{..}

    View Slide

  109. fun Modifier.focusable(*.) = composed {
    var isFocused by remember { mutableStateOf(false) }
    }
    class FocusableNode : Modifier.Node, *. {
    private var isFocused = false
    // private var isFocused by mutableStateOf(false)
    }
    Recompositionを実行させたい場合
    remember{..}

    View Slide

  110. CompositionLocalの値を取得
    Modifier.Node に書き換えるチップス

    View Slide

  111. fun Modifier.focusable(*.) = composed {
    val context = LocalContext.current
    ** *. */
    }
    CompositionLocal の値を取得
    class FocusableNode : Modifier.Node(),
    CompositionLocalConsumerModifierNode, *. {
    fun update() {
    val context = currentValueOf(LocalContext)
    }
    }

    View Slide

  112. val context = currentValueOf(LocalContext)
    currentValueOf を onAttach と onDetach の間でしか呼び出せない。
    CompositionLocal の値を取得

    View Slide

  113. class FocusableNode() : Modifier.Node(),
    CompositionLocalConsumerModifierNode, *. {
    init { currentValueOf(LocalContext) }
    override fun onAttach() { currentValueOf(LocalContext) }
    fun someMethod() {
    if (isAttached) { currentValueOf(LocalContext) }
    }
    }
    “Cannot read CompositionLocal because the
    Modifier node is not currently attached.”
    CompositionLocal の値を取得

    View Slide

  114. class FocusableNode() : Modifier.Node(),
    CompositionLocalConsumerModifierNode, *. {
    init { currentValueOf(LocalContext) }
    override fun onAttach() { currentValueOf(LocalContext) }
    fun someMethod() {
    if (isAttached) { currentValueOf(LocalContext) }
    }
    }
    CompositionLocal の値を取得

    View Slide

  115. class FocusableNode() : Modifier.Node(),
    CompositionLocalConsumerModifierNode, *. {
    init { currentValueOf(LocalContext) }
    override fun onAttach() { currentValueOf(LocalContext) }
    fun someMethod() {
    if (isAttached) { currentValueOf(LocalContext) }
    }
    }
    CompositionLocal の値を取得

    View Slide

  116. 関数パラメータは DisposableEffect の key
    Modifier.Node に書き換えるチップス

    View Slide

  117. fun Modifier.focusable(
    interactionSource: MutableInteractionSource?
    ) = composed {
    DisposableEffect(interactionSource) {
    interactionSource.tryEmit(Focus)
    onDispose {
    interactionSource*.tryEmit(UnFocus)
    }
    }
    }
    関数パラメーターは DisposableEffect のキー

    View Slide

  118. internal class FocusableElement(
    val interactionSource: MutableInteractionSource?
    ): ModifierNodeElement() {
    override fun update(node: FocusableNode) {
    node.update(interactionSource)
    }
    }
    関数パラメーターは DisposableEffect のキー

    View Slide

  119. private class FocusableNode(
    var interactionSource: MutableInteractionSource?
    ): Modifier.Node() {
    fun update(interactionSource: MutableInteractionSource?) {
    */ onDispose ブロック
    */ メインブロック
    }
    }
    関数パラメーターは DisposableEffect のキー

    View Slide

  120. private class FocusableNode(
    var interactionSource: MutableInteractionSource?
    ): Modifier.Node() {
    fun update(interactionSource: MutableInteractionSource?) {
    interactionSource..tryEmit(UnFocus)
    interactionSource..tryEmit(Focus)
    }
    }
    関数パラメーターは DisposableEffect のキー

    View Slide

  121. private class FocusableNode(
    var interactionSource: MutableInteractionSource?
    ): Modifier.Node() {
    fun update(interactionSource: MutableInteractionSource?) {
    if (this.interactionSource .= interactionSource) {
    ** interactionSource*.tryEmit(UnFocus) *. */
    this.interactionSource = interactionSource
    }
    }
    }
    関数パラメーターは DisposableEffect のキー

    View Slide

  122. LaunchedEffect{..}
    Modifier.Node に移行するチップス

    View Slide

  123. fun Modifier.draggable(state: DraggableState) = composed {
    LaunchedEffect(state) {
    interactionSource*.emit(Start())
    ** *. */
    }
    }
    LaunchedEffect{..}
    DisposableEffect と同様で LaunchefEffect の key
    が変更されたら、LaunchedEffect の中の coroutine が実行されます。

    View Slide

  124. class DraggableNode(var state: DraggableState): *. {
    fun update(state: DraggableState) {
    if(this.state *= state) {
    coroutineScope.launch {
    interactionSource*.emit(Start())
    }
    }
    }
    }
    LaunchedEffect{..}

    View Slide

  125. coroutineScope を使って onAttach と onDetach の間でしか
    coroutine を実行することができないです。
    interface Modifier {
    abstract class Node {
    val coroutineScope: CoroutineScope
    ** *. */
    }
    }
    interface Modifier {
    abstract class Node {
    val coroutineScope: CoroutineScope
    ** *. */
    }
    }
    LaunchedEffect{..}

    View Slide

  126. class FocusableNode() : Modifier.Node(), *. {
    init { coroutineScope.launch{*.} }
    override fun onAttach() { coroutineScope.launch{*.} }
    fun update() {
    if (isAttached) { coroutineScope.launch{*.} }
    }
    }
    “Cannot obtain node coordinator. Is
    the Modifier.Node attached?”
    LaunchedEffect{..}

    View Slide

  127. class FocusableNode() : Modifier.Node(), *. {
    init { coroutineScope.launch{*.} }
    override fun onAttach() { coroutineScope.launch{*.} }
    fun update() {
    if (isAttached) { coroutineScope.launch{*.} }
    }
    }
    LaunchedEffect{..}

    View Slide

  128. class FocusableNode() : Modifier.Node(), *. {
    init { coroutineScope.launch{*.} }
    override fun onAttach() { coroutineScope.launch{*.} }
    fun update() {
    if (isAttached) { coroutineScope.launch{*.} }
    }
    }
    LaunchedEffect{..}

    View Slide

  129. 複雑な @Composable 関数
    Modifier.Node に移行するチップス

    View Slide

  130. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidth by animateDpAsState(target)
    ** *. */
    }
    複雑な @Composable 関数

    View Slide

  131. @Composable
    fun animateDpAsState(targetValue: Dp): State {
    val animatable = remember { Animatable(targetValue, *.) }
    LaunchedEffect(targetValue) { ** *. */ }
    }
    複雑な @Composable 関数

    View Slide

  132. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidth by animateDpAsState(target)
    ** *. */
    }
    fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidthAnim = remember{ Animatable(target) }
    LaunchedEffect(target){ .* .. ./ }
    }
    複雑な @Composable 関数

    View Slide

  133. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidthAnim = remember{ Animatable(target) }
    ** *. */
    }
    class TabIndicatorOffsetNode(var target: Dp):Modifier.Node() {
    val currentTabWidthAnim = Animatable(..)
    ** *. */
    }
    複雑な @Composable 関数

    View Slide

  134. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidthAnim = remember{ Animatable(target) }
    LaunchedEffect(target){ .* .. ./ }
    }
    class TabIndicatorOffsetNode(var target: Dp):Modifier.Node() {
    val currentTabWidthAnim = Animatable(*.)
    fun update(target: Dp) {
    if (target .= this.target) {
    coroutineScope.launch { .* .. ./ }
    }
    }
    }
    複雑な @Composable 関数

    View Slide

  135. Modifier.Node に移行するチップス
    ● remember されてるオブジェクトを var や val として保持します。
    ● 関数パラメータに関する実装を ModifierNodeElement.update メソッドで行い
    ます。
    ● key になっている変数を state として保持する
    ● CompositionLocalConsumerModifierNode を拡張して
    CompositionLocal の値を取得します。
    ● 複雑な @Composable の場合、関数を内部実装に置き換えるように想像して複
    製してみる。
    まとめ

    View Slide

  136. 移行の事例
    既存の Modifier.composed を移行する

    View Slide

  137. Enter QR HERE
    fun Modifier.tabIndicatorOffset(
    tabPosition: TabPosition
    ) = composed { ** *. */ }
    PRのリンク
    移行の事例

    View Slide

  138. class TabIndicatorOffsetElement
    : ModifierNodeElement()
    class TabIndicatorOffsetNode(..) : Modifier.Node()
    fun Modifier.tabIndicatorOffset(tabPosition: TabPosition)
    = composed { ** *. */ }
    移行の事例
    ModifierNodeElement と Modifier.Node クラスを作ります。

    View Slide

  139. class TabIndicatorOffsetElement(val tabPosition: TabPosition)
    : ModifierNodeElement()
    class TabIndicatorOffsetNode(*.) : Modifier.Node()
    fun Modifier.tabIndicatorOffset(tabPosition: TabPosition)
    = composed { ** *. */ }
    fun Modifier.tabIndicatorOffset(tabPosition: TabPosition)
    = composed { ** *. */ }
    fun Modifier.tabIndicatorOffset(tabPosition: TabPosition)
    = then(TabIndicatorOffsetElement(tabPosition))
    移行の事例
    ModifierNodeElement に関数パラメータの tabPosition を
    コンストラクターパラメータとして設定

    View Slide

  140. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidth by animateDpAsState(..)
    ** *. */
    Modifier*.
    .width(currentTabWidth)
    }
    移行の事例

    View Slide

  141. @Composable
    fun animateDpAsState(targetValue: Dp): State {
    val animatable = remember { Animatable(targetValue, *.) }
    ** *. */
    return animatable.asState()
    }
    移行の事例

    View Slide

  142. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidth by animateDpAsState(*.)
    ** *. */
    }
    fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidthAnim = remember { Animatable(..) }
    val currentTabWidth by currentTabWidthAnim.asState()
    ** *. */
    }
    移行の事例

    View Slide

  143. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidthAnim = remember { Animatable(..) }
    val currentTabWidth by currentTabWidthAnim.asState()
    ** *. */
    }
    class TabIndicatorOffsetNode(tabPosition: Position)
    : Modifier.Node() {
    val currentTabWidthAnim = Animatable(..)
    }
    移行の事例

    View Slide

  144. @Composable
    fun animateDpAsState(targetValue: Dp): State {
    val animatable = remember { Animatable(targetValue, *.) }
    LaunchedEffect(targetValue) {
    if (targetValue .= animatable.targetValue) {
    animatable.animateTo(targetValue)
    }
    }
    }
    移行の事例

    View Slide

  145. fun Modifier.tabIndicatorOffset(tabPosition: TabPosition)
    = composed {
    val tabWidth
    by animateDpAsState(targetValue = tabPosition.width)
    ** *. */
    }
    fun Modifier.tabIndicatorOffset(tabPosition: TabPosition)
    = composed {
    val tabWidthAnim = remember { Animatable(*.) }
    LaunchedEffect(tabPosition.width) {
    if (tabPosition.width *= tabWidthAnim.targetValue) {
    tabWidthAnim.animateTo(tabPosition.width)
    }
    }
    }
    移行の事例

    View Slide

  146. class TabIndicatorOffsetNode(tabPosition: TabPosition): *. {
    val tabWidthAnim = Animatable(*.)
    fun update(tabPosition: TabPosition) {
    if (tabPosition.width *= tabWidhtAnim.targetValue) {
    coroutineScope.launch {
    tabWidthAnim.animateTo(tabPosition.width)
    }
    }
    }
    fun Modifier.tabIndicatorOffset(tabPosition: TabPosition)
    = composed {
    val tabWidthAnim = remember { Animatable(*.) }
    LaunchedEffect(tabPosition.width) {
    if (tabPosition.width *= tabWidthAnim.targetValue) {
    tabWidthAnim.animateTo(tabPosition.width)
    }
    }
    }
    移行の事例

    View Slide

  147. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidth by animateDpAsState(*.)
    ** *. */
    Modifier*.
    .width(currentTabWidth)
    }
    移行の事例

    View Slide

  148. private class SizeNode(var width: Dp)
    : LayoutModifierNode, Modifier.Node() {
    override fun MeasureScope.measure(..): MeasureResult {
    val wrappedConstraints = constraints.constrain(
    Constraints(minWidth = width, maxWidth = width)
    )
    }
    ** *. */
    }
    移行の事例
    class TabIndicatorOffsetNode(*.) : LayoutModifierNode, *. {
    override fun MeasureScope.measure(..): MeasureResult {
    val width = .?
    val wrappedConstraints = constraints.constrain(
    Constraints(minWidth = width, maxWidth = width)
    )
    }
    ** *. */
    }

    View Slide

  149. class TabIndicatorOffsetNode(*.) : LayoutModifierNode, *. {
    override fun MeasureScope.measure(..): MeasureResult {
    val width = .?
    val wrappedConstraints = constraints.constrain(
    Constraints(minWidth = width, maxWidth = width)
    )
    }
    }
    移行の事例

    View Slide

  150. fun Modifier.tabIndicatorOffset(*.) = composed {
    val currentTabWidth by animateDpAsState(..)
    ** *. */
    Modifier*.
    .width(currentTabWidth)
    }
    移行の事例

    View Slide

  151. class TabIndicatorOffsetNode(*.) : LayoutModifierNode, *. {
    val tabWidthAnim = Animatable(..)
    override fun MeasureScope.measure(*.): MeasureResult {
    val width = tabWidthAnim.value.roundToPx()
    val wrappedConstraints = constraints.constrain(
    Constraints(minWidth = width, maxWidth = width)
    )
    ** *. */
    }
    }
    移行の事例

    View Slide

  152. private class SizeNode(var width: Dp): LayoutModifierNode, *. {
    override fun MeasureScope.measure(*.): MeasureResult {
    val wrappedConstraints = ** *. */
    val placeable = measurable.measure(wrappedConstraints)
    return layout(placeable.width, placeable.height) {
    placeable.placeRelative(0, 0)
    }
    }
    }
    class TabIndicatorOffsetNode(*.) : LayoutModifierNode, *. {
    override fun MeasureScope.measure(*.): MeasureResult {
    ** *. */
    val placeable = measurable.measure(wrappedConstraints)
    return layout(placeable.width, placeable.height) {
    placeable.placeRelative(0, 0)
    }
    }
    }
    移行の事例

    View Slide

  153. 参考資料・リンク

    View Slide

  154. ● android-review.googlesource.com
    ● cs.android.com/
    ● Jetpack Compose internals
    ● romannurik.github.io/SlidesCodeHighlighter
    ● slack-chats.kotlinlang.org/c/compose
    参考資料・リンク

    View Slide

  155. Enter QR HERE
    記事のリンク
    Modifier.Node 使いましょう

    View Slide

  156. ありがとうございました!

    View Slide