Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[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

Transcript

  1. @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
  2. Row Text(Hi) Sample Text(App) remember Image @Composable fun Sample(){ var

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

    x by remember {*.} Text("Hi") Row{ Text("App") Image(*.) } } Runtime の Smart Recomposition
  4. 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
  5. 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
  6. @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
  7. @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
  8. @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)
  9. @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 の軽く紹介
  10. @Composable fun Text(text: String, *.) Text(text = "Hi") → Text(text

    = "Hi") Text(text = "Hi") → Text(text = "Hello") スキップされます スキップされない Smart Recomposition の条件
  11. Modifier.background(color: Color, *.) = this.then(Background(color, ..)) Background 旧 Modifier 仕組み

    Modifier はそれぞれの Modifier.Element をオブジェクト を発行しました。
  12. Modifier.background(color: Color, *.) .padding(start: Dp, *.) = this.then(Background(color, ..)) .then(PaddingElement(start,

    ..)) PaddingElement Background 複数の場合、発行されたそれぞれの Modifier.Element が 順次的に連結されました。 旧 Modifier 仕組み
  13. Background(Red) PointerInputElement PaddingElement(16) drawRect(color = Red) onPointerEvent { .. }

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

    . . Background(Blue) PaddingElement 旧 Modifier 仕組み
  15. Modifier.composed( factory: @Composable () *> Modifier, *. ) = this.then(ComposedModifier(factory,

    *.)) Modifier に state を持たせるため、 Modifier.composed が追加されました。 Modifier.composed が追加された理由
  16. fun Modifier.clickable(*.) = this.composed { var enabled by remember{ mutableStateOf(false)

    } val clickableState by remember{ ClickableState() } return Modifier**.onPointerEvent{ ** *. */ } } Compose Runtime を活用して レイアウト毎のstate を保持することができました。 Modifier.composed が追加された理由
  17. @Composable fun Box(*., content: @Composable () .> Unit) キャッシュされます Modifier.composed

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

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

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

    } val state = remember{ ClickableState() } LaunchedEffect{ ** *. */ } ** *. */ } @Composable 関数が Composition ツリーに追加され、 Runtime の観察するコストが増えました。 Compositionツリーが大きくなった
  22. Box Box 34 remember + 11 Side effects.. Box(modifier =

    Modifier..clickable{..}) Compositionツリーが大きくなった
  23. Box Box Box Box Box Box Box Box Box Box

    Compositionツリーが大きくなった
  24. Modifier チェーンの役割 • レイアウトにそれぞれの特定なプロパティを提供。 • 「Modifier が変更された」の判断のため比較。 Modifier に state

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

    チェーン 2つのチェーン 新しい Modifier 仕組みで 旧 Modifier チェーンが2つのチェーンに分けられました。
  26. ModifierNodeElement チェーン BackgroundElement PointerInputElement PaddingElement BackgroundNode PointerInputNode PaddingNode Box レイアウトにそれぞれの特定なプロパ

    ティを提供します。 レイアウトが Node チェーンの1つのイン スタンスを持ち続けます。 Node チェーン 2つのチェーン
  27. Node チェーン BackgroundElement PointerInputElement PaddingElement Box Element チェーン BackgroundNode PointerInputNode

    PaddingNode 「Modifier が変更された」の判断のため に比較されます。 毎回 Element 新しいインスタンスが生 成されます。 2つのチェーン
  28. interface Modifier { abstract class Node { open fun onAttach()

    {} open fun onDetach() {} var isAttached: Boolean private set ** *. */ } } Modifier.Node がチェーンに追加さ れる直後に呼び出されます。 Modifier.Node
  29. Box(modifier = Modifier .background(Color.Blue) .clickable{ .* .. ./ } .padding(16.dp))

    Box(modifier = Modifier .background(Color.Blue) .padding(16.dp)) Modifier.Node
  30. interface Modifier { abstract class Node { open fun onAttach()

    {} open fun onDetach() {} var isAttached: Boolean private set ** *. */ } } Modifier.Node チェーンから外れた 直前に呼び出されます。 Modifier.Node
  31. Box(modifier = Modifier .background(Color.Blue) .clickable{ ** *. */ } .padding(16.dp))

    Box(modifier = Modifier .background(Color.Blue) ./.clickable{ .* .. ./ } .padding(16.dp)) Modifier.Node
  32. 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
  33. abstract class ModifierNodeElement<N: Modifier.Node> : Modifier.Element { ** *. */

    } ModifierNodeElement • レイアウトを直接的に影響しないです。 • 「Modifier が変更された」の判断のために比較されます。
  34. abstract class ModifierNodeElement<N: Modifier.Node> { abstract fun create(): N abstract

    fun update(node: N) abstract fun equals(other: Any?) abstract fun hashCode(): Int ** *. */ } Modifier がはじめてレイアウトに設定された時呼び出されます。 新しい Modifier.Node のインスタンスを返します。 ModifierNodeElement
  35. Modifier が変更された時呼び出されます。 既存の Modifier.Node で保持されてる state を更新するチャンスです。 Modifier が変更された時呼び出されます。 abstract

    class ModifierNodeElement<N: Modifier.Node> { abstract fun create(): N abstract fun update(node: N) abstract fun equals(other: Any?) abstract fun hashCode(): Int ** *. */ } ModifierNodeElement
  36. abstract class ModifierNodeElement<N: Modifier.Node> { abstract fun create(): N abstract

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

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

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

    関数自体で Modifier.then に ModifierNodeElement のインスタンスを渡します。 ModifierNodeElement、Modifier.Nodeクラスの作成
  40. fun Modifier.focusable(*.) = composed { var isFocused = remember {

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

    mutableStateOf(false) } DisposableEffect(isFocused) { ** *. */ } Modifier .semantics { .* .. ./ } .then(FocusedBoundsModifier()) .onFocusChanged { .* .. ./ } } Modifier.Node の実装
  42. private class FocusChangedNode(*.) : FocusEventModifierNode { override fun onFocusEvent(focusState: FocusState)

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

    FocusState) { ** *. */ } } Modifier.Node の実装 同様で新しい Modifier.Node を FocusEventModifierNode に拡張され、onFocusEvent で実装が行われてます。
  44. 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) } }
  45. 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 に渡されたラムダ の実装が行われます。
  46. ModifierNodeElement の実装 private class FocusableElement( val interactionSource : MutableInteractionSource? )

    : ModifierNodeElement<FocusableNode> { override fun create(): FocusableNode = FocusableNode(this.interactionSource) ** *. */ }
  47. private class FocusableElement( val interactionSource : MutableInteractionSource? ) : ModifierNodeElement<FocusableNode>

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

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

    { ** *. */ override fun equals(other: Any?): Boolean { return other is FocusableElement .& other.interactionSource .= this.interactionSource } } ModifierNodeElement の実装
  50. 基本手順 • Modifier.Node と ModifierNodeElement クラスを作成する。 • Modifier の内部実装と組み合わて作られてる Modifier

    の 内部実装を複製する ような実装を Modifier.Node で行う。 • ModifierNodeElement の create で Modifier.Node の作成、update で Modifier.Node の state を更新とModifier が比較される判断実装を行う。 • Modifier.composed の代わりに Modifier.then 使って、 ModifierNodeElement のインスタンスを渡す。 まとめ
  51. fun Modifier.focusable(*.) = composed { val bringIntoViewRequester = remember {

    BringIntoViewRequester() } } class FocusableNode : Modifier.Node, *. { private val bringIntoViewRequester = BringIntoViewRequester() } remember{..}
  52. 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{..}
  53. fun Modifier.focusable(*.) = composed { val context = LocalContext.current **

    *. */ } CompositionLocal の値を取得 class FocusableNode : Modifier.Node(), CompositionLocalConsumerModifierNode, *. { fun update() { val context = currentValueOf(LocalContext) } }
  54. 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 の値を取得
  55. class FocusableNode() : Modifier.Node(), CompositionLocalConsumerModifierNode, *. { init { currentValueOf(LocalContext)

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

    } override fun onAttach() { currentValueOf(LocalContext) } fun someMethod() { if (isAttached) { currentValueOf(LocalContext) } } } CompositionLocal の値を取得
  57. fun Modifier.focusable( interactionSource: MutableInteractionSource? ) = composed { DisposableEffect(interactionSource) {

    interactionSource.tryEmit(Focus) onDispose { interactionSource*.tryEmit(UnFocus) } } } 関数パラメーターは DisposableEffect のキー
  58. internal class FocusableElement( val interactionSource: MutableInteractionSource? ): ModifierNodeElement<FocusableNode>() { override

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

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

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

    update(interactionSource: MutableInteractionSource?) { if (this.interactionSource .= interactionSource) { ** interactionSource*.tryEmit(UnFocus) *. */ this.interactionSource = interactionSource } } } 関数パラメーターは DisposableEffect のキー
  62. fun Modifier.draggable(state: DraggableState) = composed { LaunchedEffect(state) { interactionSource*.emit(Start()) **

    *. */ } } LaunchedEffect{..} DisposableEffect と同様で LaunchefEffect の key が変更されたら、LaunchedEffect の中の coroutine が実行されます。
  63. class DraggableNode(var state: DraggableState): *. { fun update(state: DraggableState) {

    if(this.state *= state) { coroutineScope.launch { interactionSource*.emit(Start()) } } } } LaunchedEffect{..}
  64. coroutineScope を使って onAttach と onDetach の間でしか coroutine を実行することができないです。 interface Modifier

    { abstract class Node { val coroutineScope: CoroutineScope ** *. */ } } interface Modifier { abstract class Node { val coroutineScope: CoroutineScope ** *. */ } } LaunchedEffect{..}
  65. 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{..}
  66. class FocusableNode() : Modifier.Node(), *. { init { coroutineScope.launch{*.} }

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

    override fun onAttach() { coroutineScope.launch{*.} } fun update() { if (isAttached) { coroutineScope.launch{*.} } } } LaunchedEffect{..}
  68. @Composable fun animateDpAsState(targetValue: Dp): State<Dp> { val animatable = remember

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

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

    } ** *. */ } class TabIndicatorOffsetNode(var target: Dp):Modifier.Node() { val currentTabWidthAnim = Animatable(..) ** *. */ } 複雑な @Composable 関数
  71. 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 関数
  72. Modifier.Node に移行するチップス • remember されてるオブジェクトを var や val として保持します。 •

    関数パラメータに関する実装を ModifierNodeElement.update メソッドで行い ます。 • key になっている変数を state として保持する • CompositionLocalConsumerModifierNode を拡張して CompositionLocal の値を取得します。 • 複雑な @Composable の場合、関数を内部実装に置き換えるように想像して複 製してみる。 まとめ
  73. class TabIndicatorOffsetElement : ModifierNodeElement<TabIndicatorOffsetNode>() class TabIndicatorOffsetNode(..) : Modifier.Node() fun Modifier.tabIndicatorOffset(tabPosition:

    TabPosition) = composed { ** *. */ } 移行の事例 ModifierNodeElement と Modifier.Node クラスを作ります。
  74. class TabIndicatorOffsetElement(val tabPosition: TabPosition) : ModifierNodeElement<TabIndicatorOffsetNode>() 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 を コンストラクターパラメータとして設定
  75. @Composable fun animateDpAsState(targetValue: Dp): State<Dp> { val animatable = remember

    { Animatable(targetValue, *.) } ** *. */ return animatable.asState() } 移行の事例
  76. fun Modifier.tabIndicatorOffset(*.) = composed { val currentTabWidth by animateDpAsState(*.) **

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

    Animatable(..) } val currentTabWidth by currentTabWidthAnim.asState() ** *. */ } class TabIndicatorOffsetNode(tabPosition: Position) : Modifier.Node() { val currentTabWidthAnim = Animatable(..) } 移行の事例
  78. @Composable fun animateDpAsState(targetValue: Dp): State<Dp> { val animatable = remember

    { Animatable(targetValue, *.) } LaunchedEffect(targetValue) { if (targetValue .= animatable.targetValue) { animatable.animateTo(targetValue) } } } 移行の事例
  79. 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) } } } 移行の事例
  80. 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) } } } 移行の事例
  81. 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) ) } ** *. */ }
  82. class TabIndicatorOffsetNode(*.) : LayoutModifierNode, *. { override fun MeasureScope.measure(..): MeasureResult

    { val width = .? val wrappedConstraints = constraints.constrain( Constraints(minWidth = width, maxWidth = width) ) } } 移行の事例
  83. 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) ) ** *. */ } } 移行の事例
  84. 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) } } } 移行の事例