Slide 1

Slide 1 text

© GO Inc. 実践 脱Modifier.composed 2023.11.14 / potatotips #85 YAMAMOTO Kohei GO株式会社

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

© 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

Slide 6

Slide 6 text

© 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()

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

© 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()

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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