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

既存画面の Jetpack Composeでの書き換え: FAANSでの事例紹介 / Case...

既存画面の 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. 8 • Fashion Advisors are Neighbors略
 •

    「ショップスタッフの
 効率的な販売をサポートする
 ショップスタッフ専用ツール」
 • Web, iOS, Androidで提供
 FAANSとは
 プレスリリース: ZOZOTOWNとブランド実店舗をつなぐOMOプラットフォーム「ZOZOMO」始動 - 株式会社ZOZO, https://corp.zozo.com/news/20211028-16352/
  6. © ZOZO, Inc. 15 • FAANSとは
 • FAANSの開発チーム
 • FAANS

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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


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

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


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

  19. © 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
  20. © ZOZO, Inc. 35 • 複数のLiveDataが公開
 ◦ 状態の公開は1つに制限
 ◦ データは1つのデータクラスにまとめる


    ◦ Eventクラスでラップした値(1度だけ処理したい)
 ▪ 状態とは別に公開
 
 コーディネート詳細: データの流れの整理
 
 private val _navigateToEditCoordinate = MutableLiveData<Event<CoordinateDetail>>() override val navigateToEditCoordinate: LiveData<Event<CoordinateDetail>> get() = _navigateToEditCoordinate
  21. © 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 }
  22. © 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 }
  23. © 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 }
  24. © ZOZO, Inc. 39 • UIへのユーザーインプット
 ◦ Actionとしてまとめる
 ◦ ActionによってStateの更新、Eventの発行を実行


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

    EditCoordinateDetail(...) : Action } fun dispatchAction(action: Action) { when (action) { is Action.EditCoordinateDetail -> _event.emit( Event.OnTransitionToEditPage(...) ) } }
  26. © 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>
  27. © ZOZO, Inc. 43 • コーディネート画像を表示する要素
 • CoordinateImageViewとしてCustomView化
 • Fragmentにロジックが直接実装


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

  28. © 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への書き換え
 

  29. © 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>, ) {...}
  30. © 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>, ) {...}
  31. © 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>
  32. © 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 = {}
  33. © 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>
  34. © ZOZO, Inc. 51 • 画面のほとんどの要素がComposeに移行
 • 残るTopAppBarをComposeへ移行
 • 画面全体をComposeで書き換える


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

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


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

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

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

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

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

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

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

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

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