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

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

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

Avatar for Ryosuke Horie

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と組み合わせて使用可能
 ◦ 既存アプリへも無理なく導入し開発を始められるのでオススメ
 まとめ