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.
    実践
    脱Modifier.composed
    2023.11.14 / potatotips #85
    YAMAMOTO Kohei
    GO株式会社

    View full-size slide

  2. © GO Inc. 2
    自己紹介
    GO株式会社
    ユーザーシステム開発部 /YAMAMOTO Kohei
    新卒でIT企業に入社後、継続的なサービスの成長に携わりたい
    と思い2021年8月に入社。
    タクシーアプリ『GO』のAndroidアプリ開発を担当。
    今秋にソロキャンデビュー。バイクは冷えるので車が欲しい。
    @farundorl @gyamoto

    View full-size slide

  3. © GO Inc. 3
    Modifier.composed と
    Modifier.Node
    01

    View full-size slide

  4. © 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,
    ),
    )

    View full-size slide

  5. © 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

    View full-size slide

  6. © 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()
    class FooNode: Modifier.Node()

    View full-size slide

  7. © GO Inc. 7
    既存のModifier.composed実装
    02

    View full-size slide

  8. © GO Inc.
    Modifier.composedを使っていた実装
    ● 既存AndroidViewから複製
    ● メイン導線用ボタンのModifier
    (一番押してほしい青いボタン)
    ● 主な用途
    ○ 「はじめてGOを利用」ボタン
    ○ 「次へすすむ」ボタン
    ○ 「タクシーを呼ぶ」ボタン
    ● タップでスケールアニメーションと
    触覚フィードバックをおこなう
    ● スケールアニメーションの状態を
    Modifier.composed で管理している
    8
    既存のModifier.composed実装

    View full-size slide

  9. © 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を使っている

    View full-size slide

  10. © 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) {
    // タップ開始〜終了でスケールアニメーション用の状態を更新する
    }
    }

    View full-size slide

  11. © GO Inc. 11
    実践 脱Modifier.composed
    03

    View full-size slide

  12. © 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()
    // Node:UI装飾や動作を設定する
    private class AnimateScaleClickableNode(
    var enabled: Boolean,
    var hapticEnabled: Boolean = true,
    ) : Modifier.Node()

    View full-size slide

  13. © 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() {
    // 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() { … }
    }

    View full-size slide

  14. © 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)

    }

    View full-size slide

  15. © 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]() }
    }
    }

    View full-size slide

  16. © 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 からスケールアニメーション用の状態を更新する
    }

    View full-size slide

  17. © 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)
    }

    View full-size slide

  18. © GO Inc. 18
    脱Modifier.composedの所感
    04

    View full-size slide

  19. © 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の所感

    View full-size slide

  20. © GO Inc. 20
    タクシーアプリ『GO』の開発を一緒にしませんか
    https://hrmos.co/pages/
    goinc/jobs

    View full-size slide

  21. 文章・画像等の内容の無断転載及び複製等の行為はご遠慮ください。
    © GO Inc.

    View full-size slide