$30 off During Our Annual Pro Sale. View Details »

既存画面の Jetpack Composeでの書き換え: FAANSでの事例紹介 / Case study of rewriting existing screens with Jetpack Compose

既存画面の Jetpack Composeでの書き換え: FAANSでの事例紹介 / Case study of rewriting existing screens with Jetpack Compose

Ryosuke Horie

May 23, 2022
Tweet

More Decks by Ryosuke Horie

Other Decks in Programming

Transcript

  1. 既存画面の
 Jetpack Composeでの書き換え:
 FAANSでの事例紹介
 2022/5/23 
 ZOZO Tech Talk #7

    - Android
 株式会社ZOZO
 ブランドソリューション開発本部 FAANS部 フロントエンド
 テックリード
 堀江 亮介 
 Copyright © ZOZO, Inc. 1
  2. © ZOZO, Inc. 株式会社ZOZO
 ブランドソリューション開発本部 FAANS部 フロントエンド
 テックリード 堀江 亮介


    • 自動化とビールが好き • 最近は家族でドライブすることが多い • @Horie1024 
 2
  3. © ZOZO, Inc. 3 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  4. © ZOZO, Inc. 4 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  5. © ZOZO, Inc. 5 2021年3月期 決算説明会補足資料 - 株式会社ZOZO, https://d31ex0fa3i203z.cloudfront.net/assets/ja/ir/pdf/webup_fy20204q_j.pdf

  6. © ZOZO, Inc. 6 2021年3月期 決算説明会補足資料 - 株式会社ZOZO, https://d31ex0fa3i203z.cloudfront.net/assets/ja/ir/pdf/webup_fy20204q_j.pdf

  7. © ZOZO, Inc. 7 2021年3月期 決算説明会補足資料 - 株式会社ZOZO, https://d31ex0fa3i203z.cloudfront.net/assets/ja/ir/pdf/webup_fy20204q_j.pdf

  8. © ZOZO, Inc. 8 • Fashion Advisors are Neighbors略
 •

    「ショップスタッフの
 効率的な販売をサポートする
 ショップスタッフ専用ツール」
 • Web, iOS, Androidで提供
 FAANSとは
 プレスリリース: ZOZOTOWNとブランド実店舗をつなぐOMOプラットフォーム「ZOZOMO」始動 - 株式会社ZOZO, https://corp.zozo.com/news/20211028-16352/
  9. © ZOZO, Inc. 9 OMOプラットフォーム「ZOZOMO」
 2022年3月期 第2四半期 決算説明会補足資料 - 株式会社ZOZO,

    https://d31ex0fa3i203z.cloudfront.net/assets/ja/ir/pdf/J_webup_FY2021_2Q.pdf
  10. © ZOZO, Inc. 10 スタッフコーデの連携
 プレスリリース: ZOZOTOWNとブランド実店舗をつなぐOMOプラットフォーム「ZOZOMO」始動 - 株式会社ZOZO, https://corp.zozo.com/news/20211028-16352/

  11. © ZOZO, Inc. 11 実店舗在庫の連携
 2022年3月期 第2四半期 決算説明会補足資料 - 株式会社ZOZO,

    https://d31ex0fa3i203z.cloudfront.net/assets/ja/ir/pdf/J_webup_FY2021_2Q.pdf
  12. © ZOZO, Inc. 12 実店舗在庫の連携
 2022年3月期 決算説明会補足資料 - 株式会社ZOZO, https://corp.zozo.com/wp-content/uploads/2022/04/JP_22034Q.pdf

  13. © ZOZO, Inc. 13 実店舗在庫の連携
 2022年3月期 決算説明会補足資料 - 株式会社ZOZO, https://corp.zozo.com/wp-content/uploads/2022/04/JP_22034Q.pdf

  14. © ZOZO, Inc. 14 実店舗在庫の連携
 2022年3月期 決算説明会補足資料 - 株式会社ZOZO, https://corp.zozo.com/wp-content/uploads/2022/04/JP_22034Q.pdf

  15. © ZOZO, Inc. 15 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  16. © ZOZO, Inc. 16 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  17. © ZOZO, Inc. 17 FAANSの開発チーム
 Miroを見る

  18. © ZOZO, Inc. 18 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  19. © ZOZO, Inc. 19 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  20. © ZOZO, Inc. 20 FAANS Androidの技術スタック
 Miroを見る

  21. © ZOZO, Inc. 21 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  22. © ZOZO, Inc. 22 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  23. © ZOZO, Inc. 23 • FAANSでは去年の8月に導入
 • フルComposeではない
 ◦ アプリの基本構成はSingle

    Activity + Navigation Component
 ◦ ViewベースなUIとComposeで作られたUIが混在 
 Jetpack Compose使ってますか?

  24. © ZOZO, Inc. 24 • ZOZOでの導入状況
 ◦ ZOZOTOWNでは導入済み
 ◦ WEARは近日中


    Jetpack Compose使ってますか?
 ZOZOTOWN AndroidへのJetpack Compose導入の取り組み - ZOZO TECH BLOG, https://techblog.zozo.com/entry/zozotown-android-jetpack-compose
  25. © ZOZO, Inc. 25 • 開発効率が向上
 ◦ 直感的な宣言型API
 ◦ シンプルなコードでのUIの記述が可能


    例: UIの出し分け、リスト表示
 ◦ 既存のViewベースなUIとの相互運用が容易
 ◦ ドキュメントやサンプルコードが豊富
 • 一部の既存ViewベースUIと相性が悪い
 ◦ 例: BottomSheetDialogFragment
 Jetpack Composeを導入してどうだったか?

  26. © ZOZO, Inc. 26 • FAANSではViewベースなUIとComposeで作られたUIが混在 
 • 今後のUI実装方針をどうするか?
 ◦

    UIは基本的にComposeで開発する方針に統一
 
 FAANSでのUI実装方針

  27. © ZOZO, Inc. 27 • 画面全体を一度に書き換えるのは難しい
 ◦ リソース、タスク優先度、開発期間 etc…
 •

    相互運用APIの存在
 ◦ 既存ViewベースUIとComposeを組み合わせて実装可能
 ◦ 既存画面のUI要素を1つずつComposeへ移行可能
 ◦ https://developer.android.com/jetpack/compose/interop
 • 既存画面を段階的にComposeに書き換えていく
 既存画面のJetpack Composeでの書き換え

  28. © ZOZO, Inc. 28 書き替え事例: コーディネート詳細


  29. © ZOZO, Inc. 29 1. データの流れの整理
 2. UI要素のComposeへの書き換え
 3. 画面全体をComposeへ書き換え


    
 書き換えの流れ
 

  30. © ZOZO, Inc. 30 • UI Stateに画面の描画に必要な情報をまとめる
 • UIの状態公開は1箇所に制限
 例.

    複数のLiveDataの公開は避ける
 • 単方向データフロー(UDF)に沿わせる
 1.データの流れの整理
 
 Android Developers アプリ アーキテクチャ ガイド UIレイヤ, https://developer.android.com/jetpack/guide/ui-layer
  31. © ZOZO, Inc. 31 • 画面を複数の要素に分解
 • 要素をComposeで再実装し置換
 • 要素単位でCustomViewを作成


    CustomViewでComposeの実装をラップ
 
 2.UI要素のComposeへの書き換え
 

  32. © ZOZO, Inc. 32 • layout.xmlを削除
 • Composableのみで画面を構成
 
 3.画面全体をComposeへ書き換え


  33. © ZOZO, Inc. 33 1. データの流れの整理
 2. UI要素のComposeへの書き換え
 3. 画面全体をComposeへ書き換え


    
 書き換えの流れ
 
 • 2を複数回繰り返し、段階的にComposeへ移行
 • 最終的に画面全体をComposeへ移行

  34. © ZOZO, Inc. 34 • 複数のLiveDataが公開
 ◦ 状態の公開は1つに制限
 ◦ データは1つのデータクラスにまとめる


    
 コーディネート詳細: データの流れの整理
 
 @HiltViewModel class CoordinateDetailDelegateImpl @Inject constructor( private val faansApiRepository: FaansApiRepository ) : ViewModel(), CoordinateDetailDelegate { private val _coordinate = MutableLiveData<CoordinateDetail>() override val coordinate: LiveData<CoordinateDetail> get() = _coordinate private val _navigateToEditCoordinate = MutableLiveData<Event<CoordinateDetail>>() override val navigateToEditCoordinate : LiveData<Event<CoordinateDetail>> get() = _navigateToEditCoordinate private val _isLoading = MutableLiveData<Boolean>() override val isLoading: LiveData<Boolean> get() = _isLoading private val _hasError = MutableLiveData<ErrorType>() override val hasError: LiveData<ErrorType> get() = _hasError
  35. © ZOZO, Inc. 35 • 複数のLiveDataが公開
 ◦ 状態の公開は1つに制限
 ◦ データは1つのデータクラスにまとめる


    ◦ Eventクラスでラップした値(1度だけ処理したい)
 ▪ 状態とは別に公開
 
 コーディネート詳細: データの流れの整理
 
 private val _navigateToEditCoordinate = MutableLiveData<Event<CoordinateDetail>>() override val navigateToEditCoordinate: LiveData<Event<CoordinateDetail>> get() = _navigateToEditCoordinate
  36. © ZOZO, Inc. 36 private val _state = MutableStateFlow(State.Initial) val

    state: StateFlow<State> = _state private val _event = MutableSharedFlow<Event>() val event: SharedFlow<Event> = _event data class State( val isLoading: Boolean = false, val coordinate: CoordinateDetail? = null, val coordinateReviewComment: String? = null, val totalViewCount: Long = 0, val totalSalesAmount: Long = 0, val coordinateItems: List<CoordinateItemDetail> = listOf(), ) { companion object { val Initial = State(isLoading = true) } } sealed interface Event { data class OnTransitionToEditPage(...) : Event }
  37. © ZOZO, Inc. 37 private val _state = MutableStateFlow(State.Initial) val

    state: StateFlow<State> = _state private val _event = MutableSharedFlow<Event>() val event: SharedFlow<Event> = _event data class State( val isLoading: Boolean = false, val coordinate: CoordinateDetail? = null, val coordinateReviewComment: String? = null, val totalViewCount: Long = 0, val totalSalesAmount: Long = 0, val coordinateItems: List<CoordinateItemDetail> = listOf(), ) { companion object { val Initial = State(isLoading = true) } } sealed interface Event { data class OnTransitionToEditPage(...) : Event }
  38. © ZOZO, Inc. 38 private val _state = MutableStateFlow(State.Initial) val

    state: StateFlow<State> = _state private val _event = MutableSharedFlow<Event>() val event: SharedFlow<Event> = _event data class State( val isLoading: Boolean = false, val coordinate: CoordinateDetail? = null, val coordinateReviewComment: String? = null, val totalViewCount: Long = 0, val totalSalesAmount: Long = 0, val coordinateItems: List<CoordinateItemDetail> = listOf(), ) { companion object { val Initial = State(isLoading = true) } } sealed interface Event { data class OnTransitionToEditPage(...) : Event }
  39. © ZOZO, Inc. 39 • UIへのユーザーインプット
 ◦ Actionとしてまとめる
 ◦ ActionによってStateの更新、Eventの発行を実行


    
 コーディネート詳細: データの流れの整理
 
 fun onClickEditCoordinate(coordinate: CoordinateDetail) { _navigateToEditCoordinate.value = Event(coordinate) }
  40. © ZOZO, Inc. 40 sealed interface Action { data class

    EditCoordinateDetail(...) : Action } fun dispatchAction(action: Action) { when (action) { is Action.EditCoordinateDetail -> _event.emit( Event.OnTransitionToEditPage(...) ) } }
  41. © ZOZO, Inc. 41 • データの流れが単方向(UDF)になるよう調整
 
 コーディネート詳細: データの流れの整理
 


  42. © ZOZO, Inc. 42 • Composeの実装をラップしたCustomViewで各要素を置換
 
 コーディネート詳細: UI要素のComposeへの書き換え
 


    <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView...> <FrameLayout...> <FrameLayout...> <TextView...> <ImageView...> <TextView...> <TextView...> <jp.faans.customview.WearItemView ...> <TextView...> <TextView...> <View...> </androidx.constraintlayout.widget.ConstraintLayout>
  43. © ZOZO, Inc. 43 • コーディネート画像を表示する要素
 • CoordinateImageViewとしてCustomView化
 • Fragmentにロジックが直接実装


    ◦ まずロジックをCustomViewに移動
 ◦ リファクタリング後Composeへの置換開始
 
 コーディネート詳細: UI要素のComposeへの書き換え
 

  44. © ZOZO, Inc. 44 • AbstractComposeViewを継承してCustomViewを作成
 • Content関数でComposeを実装
 class CoordinateImageView

    @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { @Composable override fun Content() { FaansTheme { CoordinateImage(...) } } } @Composable fun CoordinateImage(...) {} コーディネート詳細: UI要素のComposeへの書き換え
 

  45. © ZOZO, Inc. 45 class CoordinateImageView @JvmOverloads constructor( context: Context,

    attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var coordinate by mutableStateOf<CoordinateDetail?>(null) var coordinateItems = mutableStateListOf<CoordinateItemDetail>() @Composable override fun Content() { FaansTheme { CoordinateImage(coordinate, coordinateItems) } } } @Composable fun CoordinateImage( coordinate: CoordinateDetail?, coordinateItems: List<CoordinateItemDetail>, ) {...}
  46. © ZOZO, Inc. 46 class CoordinateImageView @JvmOverloads constructor( context: Context,

    attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var coordinate by mutableStateOf<CoordinateDetail?>(null) var coordinateItems = mutableStateListOf<CoordinateItemDetail>() @Composable override fun Content() { FaansTheme { CoordinateImage(coordinate, coordinateItems) } } } @Composable fun CoordinateImage( coordinate: CoordinateDetail?, coordinateItems: List<CoordinateItemDetail>, ) {...}
  47. © ZOZO, Inc. 47 • 通常のCustomViewと同様にlayout.xmlに記述
 • データを渡したい場合も同様に可能
 • CustomViewの利用側はComposeで実装されているかを


    意識する必要が無い
 binding.coordinateImageView.run { this.coordinate = coordinate coordinateItems.addAll(state.coordinateItems) } コーディネート詳細: UI要素のComposeへの書き換え
 
 <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <jp.faans.customview.CoordinateImageView ...> . . . </androidx.constraintlayout.widget.ConstraintLayout>
  48. © ZOZO, Inc. 48 • Callbackを処理したい場合は?
 • 関数型でStateを宣言し結果をラムダで受け取る
 コーディネート詳細: UI要素のComposeへの書き換え


    
 class MyButtonView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var onClick by mutableStateOf<() -> Unit>({}) @Composable override fun Content() { FaansTheme { MyButton(onClick) } } } binding.myButtonView.onClick = {}
  49. © ZOZO, Inc. 49 • CoordinateImageViewと同様に各要素をCustomView化
 • layout.xmlを置き換えていく
 コーディネート詳細: UI要素のComposeへの書き換え


  50. © ZOZO, Inc. 50 <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <jp.faans.customview.CoordinateImageView...> <jp.faans.customview.CoordinateCountsView...> <jp.faans.customview.ReviewCommentView...>

    <jp.faans.customview.TextAboutCoordinateView...> <jp.faans.customview.PublishDateView...> <jp.faans.customview.CoordinateItemAndTagView...> </androidx.constraintlayout.widget.ConstraintLayout>
  51. © ZOZO, Inc. 51 • 画面のほとんどの要素がComposeに移行
 • 残るTopAppBarをComposeへ移行
 • 画面全体をComposeで書き換える


    コーディネート詳細: 画面全体をComposeへ書き換え
 
 @Composable fun CoordinateDetailScreen( state: State = State.Initial, actionDispatcher: (Action) -> Unit = { _ -> }, ) { Scaffold( topBar = { TopAppBar(state, actionDispatcher) } ) {...} }
  52. © ZOZO, Inc. 52 Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()),

    ) { CoordinateImage(...) if (!state.coordinateReviewComment.isNullOrEmpty()) { ReviewComment(...) } else { CoordinateCounts(...) } TextAboutCoordinate(...) PublishDate(...) CoordinateItemAndTag(...) }
  53. © ZOZO, Inc. 53 • 画面全体をComposeで置換完了
 • layout.xmlは削除
 • CustomViewも削除


    • 実装したComposableだけが残る
 コーディネート詳細: 画面全体をComposeへ書き換え
 

  54. © ZOZO, Inc. 54 class CoordinateImageView @JvmOverloads constructor( context: Context,

    attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var coordinate by mutableStateOf<CoordinateDetail?>(null) var coordinateItems = mutableStateListOf<CoordinateItemDetail>() @Composable override fun Content() { FaansTheme { CoordinateImage(coordinate, coordinateItems) } } } @Composable fun CoordinateImage( coordinate: CoordinateDetail?, coordinateItems: List<CoordinateItemDetail>, ) {...}
  55. © ZOZO, Inc. 55 @Composable fun CoordinateImage( coordinate: CoordinateDetail?, coordinateItems:

    List<CoordinateItemDetail>, ) {...}
  56. © ZOZO, Inc. 56 • ComposeのNavigation Componentへ移行することで削除可能
 • 全てのFragmentをComposeのラッパーとした後移行、削除
 •

    現状FAANSではFragmentベースのNavigation Componentを使用
 
 参考: https://developer.android.com/jetpack/compose/navigation#navigate-fro m-Compose
 
 Fragment自体の削除
 

  57. © ZOZO, Inc. 57 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  58. © ZOZO, Inc. 58 • FAANSとは
 • FAANSの開発チーム
 • FAANS

    Androidの技術スタック
 • Jetpack Composeへの書き換え事例紹介
 • まとめ
 アジェンダ

  59. © ZOZO, Inc. 59 • Jetpack Composeは書いていて楽しい
 • 画面の一部の要素のみ置き換えるといった段階的な移行が可能
 ◦

    Jetpack Composeの相互運用APIは強力
 ◦ ViewベースなUIと組み合わせて使用可能
 ◦ 既存アプリへも無理なく導入し開発を始められるのでオススメ
 まとめ

  60. None