Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

@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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

@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

Slide 12

Slide 12 text

@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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

@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)

Slide 15

Slide 15 text

@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 の軽く紹介

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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 を比較できない

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Modifier.Node の紹介

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

Modifier.Node Modifier.Node の紹介

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

ModifierNodeElement Modifier.Node の紹介

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

Enter QR HERE 記事リンク ModifierNodeElement

Slide 82

Slide 82 text

既存の Modifier.composed を移行する

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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) } }

Slide 95

Slide 95 text

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 に渡されたラムダ の実装が行われます。

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

ModifierNodeElement の実装 基本手順

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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{..}

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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 の値を取得

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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{..}

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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 関数

Slide 135

Slide 135 text

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

Slide 136

Slide 136 text

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

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

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 を コンストラクターパラメータとして設定

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

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

Slide 145

Slide 145 text

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) } } } 移行の事例

Slide 146

Slide 146 text

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) } } } 移行の事例

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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) ) } ** *. */ }

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

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

Slide 151

Slide 151 text

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) ) ** *. */ } } 移行の事例

Slide 152

Slide 152 text

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) } } } 移行の事例

Slide 153

Slide 153 text

参考資料・リンク

Slide 154

Slide 154 text

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

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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