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

これでもう迷わない!Jetpack Composeの書き方実践ガイド

これでもう迷わない!Jetpack Composeの書き方実践ガイド

2025/09/11 14:20 ~ 15:00にDroidkaigi 2025の公募セッションで発表した登壇資料です。

https://2025.droidkaigi.jp/timetable/943991/

株式会社ZOZO
計測プラットフォーム開発本部
計測アプリ部 Androidブロック
中鉢 かける (@b4tchkn)

#DroidKaigi

Avatar for ZOZO Developers

ZOZO Developers PRO

September 11, 2025
Tweet

More Decks by ZOZO Developers

Other Decks in Technology

Transcript

  1. © ZOZO, Inc. 株式会社ZOZO 計測プラットフォーム開発本部 計測アプリ部 Androidブロック 中鉢 かける/ばっち 🏢2021年

    - 株式会社サイバーエージェント Flutterアプリ開発 🏢2025年 - 株式会社ZOZO ZOZOSUITを使用したAndroidアプリ開発 2
  2. © ZOZO, Inc. 8 Jetpack Composeの迷う • UIを作ったり、UIの状態管理を実装するときにどっちでも動くけどどっちがいいんだろう? • 他の画面ではこうやって実装しているけど、別の画面では同じ動作を別の方法で書かれてる?

    • modifierって絶対いるの?複雑なUIを実装するときどれくらいの単位で分割するべき?  本当は1人日で実装できるものが1.5人日、2人日かかって開発効率が落ちる  レビューに迷って速度が落ちる その結果…
  3. © ZOZO, Inc. 9 @Composable private fun ArticleCard( article: Article,

    collapsed: MutableState<Boolean>, cardModifier: Modifier = Modifier, titleModifier: Modifier = Modifier, ) { Card(modifier = cardModifier) { Padding(8.dp) { Text(article.title, modifier = titleModifier) } if (!collapsed.value) { // impl } } }
  4. © ZOZO, Inc. 12 迷いがなくなるイメージ 正しく、早くComposeを書けるように 1. 本来実現したいことに専念できるように ◦ プロダクトが実現したいものが本来あるはず

    ◦ それを効率よくできるだけ最短距離で実現する 2. バグも起きにくく、保守運用しやすいように ◦ 常にプロダクトの成長を実現する ◦ 同じ水準のコードで運用できるように https://labs.google/fx/ja/tools/whisk
  5. © ZOZO, Inc. 14 @Composable private fun ArticleCard( article: Article,

    collapsed: MutableState<Boolean>, cardModifier: Modifier = Modifier, titleModifier: Modifier = Modifier, ) { Card(modifier = cardModifier) { Padding(8.dp) { Text(article.title, modifier = titleModifier) } if (!collapsed.value) { // impl } } }
  6. © ZOZO, Inc. 16 本セッションのゴール Jetpack Composeの書き方に関して迷いが減って  本質的なことに集中できるように ✓ 作りたいものを今より早く実現できる

    ✓ それが推奨される方法で実現できる  AIと立ち向かえるように ✓ 正しい情報をAIに入力できる ✓ AIの出力を正しく判断できる
  7. © ZOZO, Inc. 17 セッションの流れ 1. Composeの基本 2. Composeのデザインパターン 3.

    パフォーマンスとアクセシビリティ 4. チーム開発と継続的な運用 それをどうやってチーム開発で継 続的に運用するか なぜ?がわかるための事前知識 それをどう活用するか実践的な プラクティス
  8. © ZOZO, Inc. 23 Composeのフェーズ Compose は、UI を更新する際に主に以下の3つのフェーズを経てフレームを描画する 1. Composition:

    「何を」 表示するかを決定するフェーズ。Composable関数を実行し、UI ツリーを 構築する 2. Layout: 「どこに」 UI 要素を配置するかを決定するフェーズ。測定と配置の2つのステップから成 り、UI ツリー内の各要素のサイズと2D座標での配置を決定する 3. Drawing: 「どのように」 UI 要素を描画するかを決定するフェーズ。UI 要素がキャンバスに実際 に描画され、画面にピクセルとしてレンダリングされる。 https://developer.android.com/develop/ui/compose/phases?hl=ja
  9. © ZOZO, Inc. 25 Unidirectional Data Flow Stateが下方に流れ、イベントが上方に流れる設計パター ン(State Hoisting)

    テストの容易性: StateとUIを分離することで、両者を切 り離してテストしやすくできる 状態のカプセル化: 状態が 1 か所でのみ更新され、コン ポーザブルの状態に関して信頼できる情報源が 1 つだけ になるため、状態の不整合によるバグが生じる可能性が 低くなる UI の整合性: StateFlowなどのオブザーバブルな状態ホル ダーを使用すると、すべての状態の更新が UI に即座に反 映される https://youtu.be/fFLBCgoHHys?feature=shared https://developer.android.com/develop/ui/compose/architecture?hl=ja#udf
  10. © ZOZO, Inc. 26 Recomposition Composeはある程度自動でスマートなRecompositionをしてくれる @Composable fun LoginScreen(showError: Boolean)

    { if (showError) { LoginError() } LoginInput() // 引数が前回と一緒なのでskip される } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ } https://developer.android.com/develop/ui/compose/lifecycle?hl=ja#skipping
  11. © ZOZO, Inc. 27 Recomposition @Composable fun LoginScreen(modifier: Modifier =

    Modifier) { var showError by remember { mutableStateOf(false) } Column(modifier = modifier) { Button( onClick = { showError = !showError }, ) { Text(text = "Click Me") } if (showError) { LoginError() } LoginInput() } }
  12. © ZOZO, Inc. 30 Modifier Composableの機能と外観を拡張するために使用される UIを構築するための中核を担う • 外部の振る舞いと外観の追加 •

    APIの簡素化 • 標準的な装飾手段 • アクセシビリティの向上 https://developer.android.com/develop/ui/compose/modifiers-list
  13. © ZOZO, Inc. 33 Composeのデザインパターン このセクションで話すこと 1. 命名規則 2. Separate

    state and events 3. コンポーネントの単一責任 4. Component or Modifier 5. 引数パラメーター
  14. © ZOZO, Inc. 34 Unitを返すComposable関数名 Unit以外を返すComposable関数名 rememberを返す関数名 CompositionLocal名 1. Composeの命名規則

    https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md
  15. © ZOZO, Inc. Pascal Caseかつ、名詞にする 35 Composeの命名規則 - Unitを返すComposable関数名 @Composable

    fun LoginScreen(/*...*/) { /*...*/ Column( modifier = modifier, ) { /*...*/ } } @Composable fun loginScreen(/*...*/) { /*...*/ } @Composable fun DrawLoginScreen(/*...*/) { /*...*/ } @Composable fun createLoginScreen(/*...*/) { /*...*/ }  GOOD  BAD
  16. © ZOZO, Inc. Camel Caseで命名するが、名詞がよくあるパターン 一部ヘルパーメソッドなど動詞ではじまるものもある 36 Composeの命名規則 - Unit以外を返すComposable関数名

    @Composable fun painterResource(@DrawableRes id: Int): Painter { /*...*/ val imageVector = loadVectorResource(/*...*/) /*...*/ return /*...*/ } @Composable private fun loadVectorResource( /*...*/ ): ImageVector { /*...*/ } https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/andro idMain/kotlin/androidx/compose/ui/res/PainterResources.android.kt?q=file:androidx%2Fcompose%2F ui%2Fres%2FPainterResources.android.kt%20function:painterResource  GOOD
  17. © ZOZO, Inc. Camel Caseで命名するが、名詞がよくあるパターン Pascal Caseにしない → UIコンポーネントとの区別を明確にするため 37

    Composeの命名規則 - Unit以外を返すComposable関数名 @Composable fun PainterResource(@DrawableRes id: Int): Painter { /*...*/ }  BAD
  18. © ZOZO, Inc. Camel Caseかつ、prefixにrememberをつける 永続性を伝えるため、remember{}を重複させないため 38 Composeの命名規則 - rememberを返す関数名

    @Composable fun rememberLazyListState( /*...*/ ): LazyListState { return rememberSaveable() { LazyListState() } } https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/fou ndation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt?q=rememberLazyLis tState  GOOD
  19. © ZOZO, Inc. Camel Caseかつ、prefixにrememberをつける 永続性を伝えるため、remember{}を重複させないため 39 Composeの命名規則 - rememberを返す関数名

    @Composable fun lazyListState(/*...*/): LazyListState { /*...*/ } @Composable fun createLazyListState(/*...*/): LazyListState { /*...*/ } @Composable fun LazyListState(/*...*/): LazyListState { /*...*/ } https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/src/commonMain/ko tlin/androidx/compose/foundation/lazy/LazyListState.kt?q=rememberLazyListState  BAD
  20. © ZOZO, Inc. Pascal Caseかつ、prefixにLocalをつける 40 Composeの命名規則 - CompositionLocal名 val

    LocalTheme = staticCompositionLocalOf<Theme>() val ThemeLocal = staticCompositionLocalOf<Theme>()  GOOD  BAD
  21. © ZOZO, Inc. 42 2. Separate state and events @Composable

    fun MyCheckBox( initialChecked: Boolean, onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { var checked by remember { mutableStateOf(initialChecked) } /*...*/ Checkbox( checked = checked, onCheckedChange = { checked = it onCheckedChange(it) }, modifier = modifier ) } Stateが2つになってしまい、 Single Source of Truthでない 別の箇所でinitialCheckedが変更されて recompositionされると意図しない動きにな る可能性がある  BAD
  22. © ZOZO, Inc. 43 2. Separate state and events @Composable

    fun MyCheckBox( checked: Boolean, onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { /*...*/ Checkbox( checked = checked, onCheckedChange = onCheckedChange, modifier = modifier ) } Single Source of TruthなStateのみで UIを構築できている State Hoistingにより、 Unidirectional Data Flowな設計  GOOD
  23. © ZOZO, Inc. 44 Composableを分割してコンポーネントに分けるとき、特にpublicなコンポーネントは分割する目的と必 要性をよく考える • 目的:コンポーネントの目的を1つにして単一責任を意識 • 必要性:存在する価値と認知負荷を考慮

    3. コンポーネントの分割と単一責任 • 存在を伝えるコストと気づかれずに重 複定義されるリスク • 柔軟性がなく、類似のコンポーネント が定義される • 既存のコンポーネントを組み合わせて 簡単につくれてしまう • インターフェイスがシンプルで汎用的 である • コンポーネントが存在する目的が1つ • 存在することで価値があることが複数 ある  GOOD  BAD
  24. © ZOZO, Inc. 45 3. コンポーネントの分割と単一責任 @Composable fun Button( //

    problem 1: button is a clickable rectangle onClick: () -> Unit = {}, // problem 2: button is a check/uncheck checkbox-like component checked: Boolean = false, onCheckedChange: (Boolean) -> Unit, ) { ... } https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#component _s-purpose  BAD
  25. © ZOZO, Inc. 46 3. コンポーネントの分割と単一責任 @Composable fun Button( onClick:

    () -> Unit, ) { /*...*/ } @Composable fun ToggleButton( checked: Boolean, onCheckedChange: (Boolean) -> Unit, ) { /*...*/ } https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#component _s-purpose  他の分割の判断基準として、分割した単位でScreenshotTestを書きたいか、Testする 価値があるコンポーネントか考えてみる  GOOD
  26. © ZOZO, Inc. 47 4. Component or Modifier horizontal paddingもRow(Spacer(),

    content(), Spacer())で作れるが、 Modifierで実現できるならModifier @Composable fun Padding(allSides: Dp) { // impl } // usage Padding(12.dp) { UserCard() UserPicture() } UserCard( modifier = Modifier.padding(12.dp) ) https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines .md#component-or-modifier  GOOD  BAD
  27. © ZOZO, Inc. 48 4. Component or Modifier @Composable fun

    RecommendCard() { // impl Card( modifier = Modifier .fillMaxSize() .background(color = Color.Red) ) { // impl } } @Composable fun RecommendCard() { // impl Box( modifier = Modifier .fillMaxSize() .background(color = Color.Red) ) Card( modifier = Modifier ) { // impl } }  BAD  GOOD
  28. © ZOZO, Inc. 49 5. 引数パラメーター @Composable fun TodoListItem( title:

    String, description: @Composable () -> Unit, isCompleted: Boolean? = null, onToggleComplete: () -> Unit, modifier: Modifier = Modifier, ) { } @Composable () -> UnitとStringの使い分け 引数の順番は? デフォルト値の nullあり・なし
  29. © ZOZO, Inc. 50 5. 引数パラメーター - 順番 1. 必須パラメータ

    2. Modifier(最初のオプショナルパラメーター) 3. それ以外のオプショナルパラメーター @Composable fun TopicScreen( showBackButton: Boolean, onBackClick: () -> Unit, onTopicClick: (String) -> Unit, modifier: Modifier = Modifier, viewModel: TopicViewModel = hiltViewModel(), ) { /*...*/ } https://github.com/android/nowinandroid/blob/main/feature /topic/src/main/kotlin/com/google/samples/apps/nowinandro id/feature/topic/TopicScreen.kt
  30. © ZOZO, Inc. 51 5. 引数パラメーター - Slot parameter Slot-based

    layoutsと言われているパターン 抽象的なインターフェイスにしてカスタマイズ性を上げてオーバーロードを削減する https://android.googlesource.com/platform/frameworks/support/+/ androidx-main/compose/docs/compose-component-api-guidelines.md #slot-parameters https://developer.android.com/develop/ui/compose/layouts/basics? hl=ja#slot-based-layouts @Composable fun TopAppBar( title: @Composable () -> Unit, /* other params */ navigationIcon: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, ) =
  31. © ZOZO, Inc. 52 5. 引数パラメーター - Slot parameter https://android.googlesource.com/platform/frame

    works/support/+/androidx-main/compose/docs/co mpose-component-api-guidelines.md#slot-paramet ers @Composable fun Button( onClick: () -> Unit, text: String? = null, icon: ImageBitmap? = null ) {} @Composable fun Button( onClick: () -> Unit, text: String? = null, fontStyle: FontStyle? = null // add icon: ImageBitmap? = null ) {} @Composable fun Button( onClick: () -> Unit, text: String? = null, fontStyle: FontStyle? = null icon: ImageVector? = null, // change ) {}  BAD
  32. © ZOZO, Inc. 53 5. 引数パラメーター - Slot parameter https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#sl

    ot-parameters @Composable fun Button( onClick: () -> Unit, text: @Composable () -> Unit, icon: @Composable () -> Unit ) {}  GOOD  privateなComposable、デザインシステムでは過度なslotは避けて厳格に実装したほう が良さそう
  33. © ZOZO, Inc. 54 5. 引数パラメーター - Slot parameter https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#sl

    ot-parameters @Composable fun Button( onClick: () -> Unit, content: @Composable () -> Unit ) {} // usage Button(onClick = { /* handle the click */}) { Row { Icon(...) Text(...) } } 複数のslot parameterがある場合、contentだけも検討する
  34. © ZOZO, Inc. 55 5. 引数パラメーター - Nullable value https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#nullabl

    e-parameter @Composable fun IconCard( bitmap: ImageBitmap, backgroundColor: Color = DefaultBackgroundColor, // デフォルト値にするかどうかの判断にnullを使わない elevation: Dp? = null ) { // デフォルト値を引数で最初からもらう val resolvedElevation = elevation ?: DefaultElevation } @Composable fun IconCard( bitmap: ImageBitmap, elevation: Dp = 8.dp ) { ... }  GOOD  BAD
  35. © ZOZO, Inc. 56 5. 引数パラメーター - ComponentElevation objects コンポーネントの特定のプロパティ(色やエレベーションなど)が、コンポーネントの異なる状態

    (enabled/disabled、focused/hovered/pressedなど)に基づいてどのように変化するかまとめる専用 のclassを検討する https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#compo nentcolor_componentelevation-objects @Composable fun Button( onClick: () -> Unit, enabled: Boolean = true, backgroundColor = if (enabled) ButtonDefaults.enabledBackgroundColor else ButtonDefaults.disabledBackgroundColor, elevation = if (enabled) ButtonDefaults.enabledElevation else ButtonDefaults.disabledElevation, content: @Composable RowScope.() -> Unit ) {}
  36. © ZOZO, Inc. 57 5. 引数パラメーター - ComponentElevation objects https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#compo

    nentcolor_componentelevation-objects class ButtonColors( backgroundColor: Color, /* other params */ disabledContentColor: Color ) { // ロジックを専用のclassに閉じ込めることで // UI本体のAPIの肥大化を防ぐ fun backgroundColor(enabled: Boolean): Color { ... } }
  37. © ZOZO, Inc. 58 5. 引数パラメーター - ComponentElevation objects https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md#compo

    nentcolor_componentelevation-objects @Composable fun Button( onClick: () -> Unit, enabled: Boolean = true, colors: ButtonColors = ButtonDefaults.colors(), content: @Composable RowScope.() -> Unit ) { val resolvedBackgroundColor = colors.backgroundColor(enabled) }
  38. © ZOZO, Inc. 59 5. 引数パラメーター - Modifier 1. 必ず引数に持つ

    2. 必ずデフォルト値を設定する 3. 引数に複数持たない 4. 引数のmodifierを必ずチェーンの先頭にする 5. 引数のmodifierをトップレベルでのみ参照する 6. Modifierで表現できる引数にデフォルト値をつけない 7. チェーン済みのModifierをデフォルト値にしない
  39. © ZOZO, Inc. 60 5. 引数パラメーター - Modifier @Composable fun

    Icon( bitmap: ImageBitmap, // no modifier parameter tint: Color = Color.Black ) @Composable fun CheckboxRow( checked: Boolean, onCheckedChange: (Boolean) -> Unit, // DON'T rowModifier: Modifier = Modifier, checkboxModifier: Modifier = Modifier ) @Composable fun Icon( bitmap: ImageBitmap, tint: Color = Color.Black, // 1: modifier is not the first optional parameter // 2: padding will be lost as soon as the user sets its own modifier modifier: Modifier = Modifier.padding(8.dp) )  BAD
  40. © ZOZO, Inc. 61 5. 引数パラメーター - Modifier @Composable fun

    IconButton( buttonBitmap: ImageBitmap, modifier: Modifier = Modifier, tint: Color = Color.Black ) { Box(Modifier.padding(16.dp)) { Icon( buttonBitmap, // modifier should be applied to the outer-most layout // and be the first one in the chain modifier = Modifier.aspectRatio(1f).then(modifier), tint = tint ) } }  BAD
  41. © ZOZO, Inc. 64 必要最低限な引数 個別のパラメータを使用すると、パフォーマンスが向上することもある News に title と

    subtitle 以上の情報が含まれている場合、News の新しいインスタンスが Header(news) に渡されるたびに、title と subtitle が変更されていなくてもコンポーザブルは再コン ポーズされる @Composable fun Header(title: String, subtitle: String) { // Recomposes when title or subtitle have changed. } @Composable fun Header(news: News) { // Recomposes when a new instance of News is passed in. } https://developer.android.com/develop/ui/compose/architecture?hl=ja#composable-parameter
  42. © ZOZO, Inc. 66 状態の読み取りを遅延させる ラムダベースのModifierを使用して読み取りを可能な限り遅延させる スクロールの度に大量にRecompositionされる @Composable private fun

    Title(snack: Snack, scroll: Int) { val offset = (maxOffset - scroll).coerceAtLeast(minOffset) Column( modifier = Modifier .graphicsLayer (translationY = offset) ) { … } }
  43. © ZOZO, Inc. 69 アクセシビリティ おそらく最もよく遭遇するアクセシビリティについて Image( painter = painterResource(id

    = R.drawable.ic_todo_icon), contentDescription = null, modifier = modifier.padding(8.dp) ) Icon( painter = painterResource(id = if (isCompleted) R.drawable.ic_check else R.drawable.ic_uncheck), contentDescription = null, modifier = Modifier.padding(8.dp) ) よくやりませんか
  44. © ZOZO, Inc. 75 シンプルで柔軟なルール • 厳格すぎるルール ◦ 柔軟性の阻害 ◦

    開発速度の低下 ◦ チームメンバーのストレス • 完璧を求めすぎないかつ、個人の書き方の自由のゆとりを持たせる ◦ 認知負荷 ◦ 持続可能性 ◦ 複雑さ
  45. © ZOZO, Inc. 78 runtime-lint いくつかの最低限のComposeルールが含まれている • Composable関数名 • CompositionLocal名

    • などなど https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/runtime/runtime-lint/src/main/java/androidx/co mpose/runtime/lint/
  46. © ZOZO, Inc. 79 slack compose-lints より高機能なComposeのLintルール • Modifierの使いまわし •

    不安定なcollection • などなど https://slackhq.github.io/compose-lints/
  47. © ZOZO, Inc. 80 AI? Lint? AI Lint 導入難易度  低い

     高い 継続コスト  高い  低い 結果の一貫性  低い  高い 文脈理解  得意  限定的 実行速度  遅い  早い
  48. © ZOZO, Inc. 82 まとめ 1. 迷いによる課題 a. 開発効率が落ちて本質的なことに向き合えない b.

    AIのアウトプットを判断するスキルが求められている 2. 解決するためのプラクティス a. Composeのデザインパターン b. パフォーマンスとアクセシビリティ c. Preview 3. プラクティスの運用について a. シンプルで柔軟なルール b. AI vs Lint
  49. © ZOZO, Inc. 83 @Composable private fun ArticleCard( article: Article,

    collapsed: MutableState<Boolean>, cardModifier: Modifier = Modifier, titleModifier: Modifier = Modifier, ) { Card(modifier = cardModifier) { Padding(8.dp) { Text(article.title, modifier = titleModifier) } if (!collapsed.value) { // impl } } }
  50. © ZOZO, Inc. 84 参照 • https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compo se/docs/compose-api-guidelines.md • https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compo

    se/docs/compose-component-api-guidelines.md • https://github.com/android/nowinandroid • https://developer.android.com/develop/ui/compose/documentation • https://m3.material.io/components/all-buttons • https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose /runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ • https://slackhq.github.io/compose-lints/