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

実践 脱Modifier.composed / Let's Modifier.Node

gyamoto
November 14, 2023

実践 脱Modifier.composed / Let's Modifier.Node

gyamoto

November 14, 2023
Tweet

More Decks by gyamoto

Other Decks in Technology

Transcript

  1. © GO Inc. 2 自己紹介 GO株式会社 ユーザーシステム開発部 /YAMAMOTO Kohei 新卒でIT企業に入社後、継続的なサービスの成長に携わりたい

    と思い2021年8月に入社。 タクシーアプリ『GO』のAndroidアプリ開発を担当。 今秋にソロキャンデビュー。バイクは冷えるので車が欲しい。 @farundorl @gyamoto
  2. © GO Inc. UI実装ツールキットであるJetpackComposeに含まれる、UI装飾や動作を設定するクラス 4 Modifier.composed と Modifier.Node Modifierとは Text(

    text = "GOタクシーを呼ぶ", modifier = Modifier .background( color = mainColor, shape = RoundedCornerShape(50), ) .padding( horizontal = 16.dp, vertical = 8.dp, ), )
  3. © GO Inc. Modifier.composed:Modifierを返すModifier タクシーアプリ『GO』ではStatefulなカスタムModifierの実装で使用 Modifier.composed は Recomposition が発生した場合のパフォーマンスに問題がある 5

    Modifier.composed と Modifier.Node Modifier.composed問題 参考 • Modifier.Node を使いましょう | DroidKaigi 2023 https://2023.droidkaigi.jp/timetable/492313/ • Compose Modifiers deep dive | Android Dev Summit 2022 https://www.youtube.com/watch?v=BjGX2RftXsU
  4. © GO Inc. Modifier.composedの代替としてModifier.Node APIが提供されている Modifier.NodeではModifierの役割を Node, Element に分担する 6

    Modifier.composed と Modifier.Node Modifier.Nodeへの移行 // BEFORE (Modifier.composed) fun Modifier.foo() = composed { … } // AFTER (Modifier.Node) fun Modifier.foo() = this.then(FooElement()) class FooElement: ModifierNodeElement<FooNode>() class FooNode: Modifier.Node()
  5. © GO Inc. Modifier.composedを使っていた実装 • 既存AndroidViewから複製 • メイン導線用ボタンのModifier (一番押してほしい青いボタン) •

    主な用途 ◦ 「はじめてGOを利用」ボタン ◦ 「次へすすむ」ボタン ◦ 「タクシーを呼ぶ」ボタン • タップでスケールアニメーションと 触覚フィードバックをおこなう • スケールアニメーションの状態を Modifier.composed で管理している 8 既存のModifier.composed実装
  6. © GO Inc. 9 既存のModifier.composed実装 @Composable fun HapticFeedbackButton(…) { Button(

    modifier = modifier.animateScaleClickable(enabled), ) } private fun Modifier.animateScaleClickable( enabled: Boolean, hapticEnabled: Boolean = true, ): Modifier = composed { … } // ここでModifier.composedを使っている
  7. © GO Inc. 10 既存のModifier.composed実装 private fun Modifier.animateScaleClickable(…): Modifier =

    composed { // タップ中〜直後の状態を管理して、スケールアニメーションする var buttonPressed by remember { mutableStateOf(false) } var isDuringAnimation by remember { mutableStateOf(false) } val scale by animateFloatAsState( if (buttonPressed || isDuringAnimation) 1f else 0.9f ) // タップ直後に触覚フィードバックする val haptic = LocalHapticFeedback.current scale(scale).pointerInput(interactionSource, enabled) { // タップ開始〜終了でスケールアニメーション用の状態を更新する } }
  8. © GO Inc. 12 実践 脱Modifier.composed Modifier.composedをNode, Elementに分担する private fun

    Modifier.animateScaleClickable( enabled: Boolean, hapticEnabled: Boolean = true, ): Modifier = this.then( AnimateScaleClickableElement(enabled, hapticEnabled) ) // Element:Modifierの変更判定をおこなう private data class AnimateScaleClickableElement( val enabled: Boolean, val hapticEnabled: Boolean = true, ) : ModifierNodeElement<AnimateScaleClickableNode>() // Node:UI装飾や動作を設定する private class AnimateScaleClickableNode( var enabled: Boolean, var hapticEnabled: Boolean = true, ) : Modifier.Node()
  9. © GO Inc. 13 実践 脱Modifier.composed ModifierNodeElementで変更判定をおこなう // equals, hashCode

    によってModifierに変更があるか判定される // (data class でプロパティから自動で実装される ) private data class AnimateScaleClickableElement( private val enabled: Boolean, private val hapticEnabled: Boolean = true, ) : ModifierNodeElement<AnimateScaleClickableNode>() { // Modifier.Nodeのインスタンスを生成する override fun create() = AnimateScaleClickableNode(enabled, hapticEnabled) // Modifierの変更がある判定の場合、 Modifier.Nodeを更新する override fun update(node: AnimateScaleClickableNode) { node.enabled = enabled node.hapticEnabled = hapticEnabled } override fun InspectorInfo.inspectableProperties() { … } }
  10. © GO Inc. 14 実践 脱Modifier.composed Modifier.NodeでUIを表現する private class AnimateScaleClickableNode(

    var enabled: Boolean, var hapticEnabled: Boolean = true, ) : Modifier.Node(), DrawModifierNode, // Modifier.scaleを実装する PointerInputModifierNode, // Modifier.pointerInputを実装する CompositionLocalConsumerModifierNode // CompositionLocalを実装する { // タップ中〜直後の状態を管理して、スケールアニメーションする var buttonPressed = mutableStateOf(false) var isDuringAnimation = mutableStateOf(false) … }
  11. © GO Inc. 15 実践 脱Modifier.composed Modifier.NodeでUIを表現する(Modifier.scale) private class AnimateScaleClickableNode(…)

    : Modifier.Node(), DrawModifierNode, // Modifier.scaleを実装する … { /* [BEFORE] val scale by animateFloatAsState(...) Modifier.scale(scale) */ override fun ContentDrawScope.draw() { val scale = if (buttonPressed.value || isDuringAnimation.value) 1f else 0.9f scale(scale) { [email protected]() } } }
  12. © GO Inc. 16 実践 脱Modifier.composed Modifier.NodeでUIを表現する(Modifier.pointerInput) private class AnimateScaleClickableNode(…)

    : Modifier.Node(), PointerInputModifierNode, // Modifier.pointerInputを実装する … { /* [BEFORE] Modifier.pointerInput(interactionSource, enabled) { // タップ開始〜終了でスケールアニメーション用の状態を更新する } */ override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize ) { // PointerEvent, PointerEventPass からスケールアニメーション用の状態を更新する }
  13. © GO Inc. 17 実践 脱Modifier.composed Modifier.NodeでUIを表現する(CompositionLocal) private class AnimateScaleClickableNode(…)

    : Modifier.Node(),… CompositionLocalConsumerModifierNode { // CompositionLocalを実装する /* [BEFORE] // タップ直後に触覚フィードバックする val haptic = LocalHapticFeedback.current haptic.performHapticFeedback(HapticFeedbackType.LongPress) */ currentValueOf(LocalHapticFeedback) .performHapticFeedback(HapticFeedbackType.LongPress) }
  14. © GO Inc. • 既存のModifier.composedは移行できそう ◦ 既存のModifier.Nodeが豊富 ▪ 参考:androidx.compose.ui.node |

    Android Developers https://developer.android.com/reference/kotlin/androidx/compose/ui/node/package-summary ◦ CompositionLocalだけでなくCoroutinesなども対応 • Modifier.composedと比べるとModifier.Nodeは複雑な実装になる • この機会に複雑なカスタムModifierを分解できれば、スムーズに移行できそう ◦ 今回のカスタムModifierの場合だと「スケールアニメーション」と「触覚フィードバック」に 分解できそう 19 脱Modifier.composedの所感