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

段階的Jetpack Compose導入 〜メルペイの場合〜 / Phased Implementation of Jetpack Compose at Merpay

段階的Jetpack Compose導入 〜メルペイの場合〜 / Phased Implementation of Jetpack Compose at Merpay

今年のGoogle I/Oでも大きく時間の割かれたJetpack Composeは、Androidアプリ開発において過去最大のパラダイムシフトです。
このセッションではメルペイのJetpack Compose導入について、メルペイが現在置かれている少し特殊な状況、その中でのJetpack Compose導入戦略と乗り越えた課題などをお話しできればと思います。
------
Merpay Tech Fest 2022は3日間のオンライン技術カンファレンスです。
IT企業で働くソフトウェアエンジニアおよびメルペイの技術スタックに興味がある方々を対象に2022年8月23日(火)から8月25日(木)までの3日間、開催します。 Merpay Tech Festは事業との関わりから技術への興味を深め、プロダクトやサービスを支えるエンジニアリングを知れるお祭りです。 セッションでは事業を支える組織・技術・課題などへの試行錯誤やアプローチを紹介予定です。お楽しみに!

■イベント関連情報
- 公式ウェブサイト:https://events.merpay.com/techfest-2022/
- 申し込みページ:https://mercari.connpass.com/event/249428/
- Twitterハッシュタグ: #MerpayTechFest
■リンク集
- メルカリ・メルペイイベント一覧:https://mercari.connpass.com/
- メルカリキャリアサイト:https://careers.mercari.com/
- メルカリエンジニアリングブログ:https://engineering.mercari.com/blog/
- メルカリエンジニア向けTwitterアカウント:https://twitter.com/mercaridevjp
- 株式会社メルペイ:https://jp.merpay.com/

mercari
PRO

August 25, 2022
Tweet

More Decks by mercari

Other Decks in Technology

Transcript

  1. 段階的Jetpack Compose導入 〜メルペイの場合〜 Junya Matsuyama 株式会社メルペイ Android Engineer / Engineering

    Manager
  2. Junya Matsuyama 株式会社メルペイ Android Engineer 兼 Engineering Manager 2021年7月にメルペイに入社 新機能開発等に従事

    それ以前は携帯電話、ソーシャルゲーム、 SNS、ライブ配 信サービス、タクシー配車サービスなど、様々なモバイル 向けのアプリ・サービスを開発
  3. Agenda 01 メルカリアプリの構造 02 Jetpack Composeの導入 03 導入の結果 & 今後

  4. 01 メルカリアプリの構造

  5. メルカリアプリ メルカリ メルペイ Design System

  6. メルカリアプリ メルカリ メルペイ Design System 初期リリースは約9 年前 蓄積した技術的負 債による開発効率 低下が大きな課題

  7. メルカリアプリ メルカリ メルペイ Design System 初期リリースは約9 年前 蓄積した技術的負 債による開発効率 低下が大きな課題

    に リリースから3年半 技術的負債はそこ まで溜まっていない
  8. メルカリアプリ メルカリ メルペイ Design System 初期リリースは約9 年前 蓄積した技術的負 債による開発効率 低下が大きな課題

    に リリースから3年半 技術的負債はそこ まで溜まっていない 新機能開発を止め て、すべて作り直す 事に
  9. メルカリアプリ メルカリ メルペイ Design System 初期リリースは約9 年前 蓄積した技術的負 債による開発効率 低下が大きな課題

    に 新機能開発を止め て、すべて作り直す 事に 事業成長のための 機能開発・改善を継 続 リリースから3年半 技術的負債はそこ まで溜まっていない
  10. じゃあどうする?

  11. 現在のリポジトリ構成 メルカリ メルペイ 新メルカリ 新Design System submodule Design System

  12. 現行メルカリアプリ メルカリ メルペイ Design System

  13. 新メルカリアプリ メルペイ 新メルカリ 新Design System submodule Design System

  14. 現在のリポジトリ構成概要 メルカリ メルペイ 新メルカリ 新Design System submodule Design System メルペイ部分の開発を止めずに、

    メルカリ部分のフルリファクタリングを可能に
  15. 未来のリポジトリ構成 Step 1 メルペイ 新メルカリ 新Design System subtree Design System

  16. 未来のリポジトリ構成 Step 1 メルペイ 新メルカリ 新Design System subtree Design System

    submoduleをやめ、 subtreeで必要なコード だけを履歴ごと取り込む。
  17. 未来のリポジトリ構成 最終予定図 メルペイ 新メルカリ 新Design System Design System

  18. 02 Jetpack Composeの導入

  19. とはいえJetpack Composeも 使っていきたいよね。

  20. Q1. UIの書き換えは可能かな?

  21. メルペイのアーキテクチャ Redux の考え方と MVVM を組み合わせた構成 ref:https://engineering.mercari.com/blog/entry/merpay-android-architecture-and-life-cycle/ bluelinelabs/Conductor というライブラリの Controller

  22. メルペイのアーキテクチャ Redux の考え方と MVVM を組み合わせた構成 ref:https://engineering.mercari.com/blog/entry/merpay-android-architecture-and-life-cycle/ Unidirectional Data Flow

  23. あれ、Jetpack Composeって…

  24. Jetpack Compose Unidirectional data flowが推奨されている ref:https://developer.android.com/jetpack/compose/architecture

  25. メルペイのアーキテクチャ Redux の考え方と MVVM を組み合わせた構成 ref:https://engineering.mercari.com/blog/entry/merpay-android-architecture-and-life-cycle/ Compopsable 基本アーキテクチャ としては、 UI部分を置き換え

    る事で導入はできそ う State Event Unidirectional Data Flow
  26. 試してみよう

  27. Before import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return inflater.inflate(R.layout. controller_sample , container, false) } ... viewModel.inputs.someEvent() // <-- UI Event ... viewModel.state.collect {...} // <-- state反映 }
  28. Before import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return inflater.inflate(R.layout.controller_sample, container, false) } ... viewModel.inputs.someEvent() // <-- UI Event ... viewModel.state.collect {...} // <-- state反映 }
  29. Before import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return inflater.inflate(R.layout. controller_sample , container, false) } ... viewModel.inputs.someEvent() // <-- UI Event ... viewModel.state.collect {...} // <-- state反映 }
  30. After import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return ComposeView(container. context).apply { setContent { ControllerRoot { // <-- Themeなどの指定をまとめたもの SampleScreen( viewModel .inputs, viewModel.state.collectAsStateWithLifecycle().value, ) }
  31. After import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return ComposeView(container.context).apply { setContent { ControllerRoot { // <-- Themeなどの指定をまとめたもの SampleScreen( viewModel .inputs, viewModel.state.collectAsStateWithLifecycle().value, ) }
  32. After import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return ComposeView(container. context).apply { setContent { ControllerRoot { // <-- Themeなどの指定をまとめたもの SampleScreen( viewModel.inputs, viewModel.state.collectAsStateWithLifecycle().value, ) }
  33. After @Composable fun SampleScreen( inputs: SampleInputs, state: SampleState, ) {

    when (state) { // 画面要素出し分け is Xxx -> { ... } is Yyy -> { SomeComponent( inputs::someEvent) } } }
  34. UI部分だけのJetpack Compose化はできそう。

  35. Q2. 画面間の遷移はどうしよう?

  36. Action : Before sealed interface SampleAction : Action { object

    SomeAction : SampleAction object AnyAction : SampleAction object HogeAction : SampleAction object PiyoAction : SampleAction object NavigateToHogeAction : SampleAction object NavigateToPiyoAction : SampleAction }
  37. Action : After sealed interface SampleNavigationAction sealed interface SampleAction :

    Action { object SomeAction : SampleAction object AnyAction : SampleAction object HogeAction : SampleAction object PiyoAction : SampleAction object NavigateToHogeAction : SampleAction, SampleNavigationAction object NavigateToPiyoAction : SampleAction, SampleNavigationAction }
  38. NavigationActionのハンドリング class SampleViewModel(...)... { val navigateAction : Flow<SampleNavigationAction> get() =

    action.filterIsInstance () } //—----------------------------------------- class SampleController : Controller() { ControllerRoot { LaunchedEffect( viewModel.navigateAction ) { viewModel.navigateAction .onEach { when (it) { NavigateToHogeAction -> { /* Hoge への遷移 */ } NavigateToPiyoAction -> { /* Piyo への遷移 */ } } }.collect() } SampleScreen(...)
  39. NavigationActionのハンドリング class SampleViewModel(...)... { val navigateAction: Flow<SampleNavigationAction> get() = action.filterIsInstance()

    } //—----------------------------------------- class SampleController : Controller() { ControllerRoot { LaunchedEffect( viewModel.navigateAction ) { viewModel.navigateAction .onEach { when (it) { NavigateToHogeAction -> { /* Hoge への遷移 */ } NavigateToPiyoAction -> { /* Piyo への遷移 */ } } }.collect() } SampleScreen(...)
  40. NavigationActionのハンドリング class SampleViewModel(...)... { val navigateAction : Flow<SampleNavigationAction> get() =

    action.filterIsInstance () } //—----------------------------------------- class SampleController : Controller() { ControllerRoot { LaunchedEffect(viewModel.navigateAction) { viewModel.navigateAction.onEach { when (it) { NavigateToHogeAction -> { /* Hogeへの遷移 */ } NavigateToPiyoAction -> { /* Piyoへの遷移 */ } } }.collect() } SampleScreen(...)
  41. 遷移もいけそう

  42. Q3. Deep Linkはどうしよう

  43. 以前のスタイル class DeepLinkActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) presenter.handleDeepLink( intent) } // Presenterの実体 override fun handleDeepLink (intent: Intent?) { val deepLinkResolver = factory.getResolver(DeepLinkType.from(uri)) deepLinkResolver?.resolve(uri) } // Factoryの実体 override fun getResolver(type: DeepLinkType): DeepLinkResolver? = when (type) { DeepLinkType. MERPAY_DEEP_LINK -> MerpayDeepLinkResolver( context) }
  44. 以前のスタイル class DeepLinkActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) presenter.handleDeepLink(intent) } // Presenterの実体 override fun handleDeepLink (intent: Intent?) { val deepLinkResolver = factory.getResolver(DeepLinkType.from(uri)) deepLinkResolver?.resolve(uri) } // Factoryの実体 override fun getResolver(type: DeepLinkType): DeepLinkResolver? = when (type) { DeepLinkType. MERPAY_DEEP_LINK -> MerpayDeepLinkResolver( context) }
  45. 以前のスタイル class DeepLinkActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) presenter.handleDeepLink( intent) } // Presenterの実体 override fun handleDeepLink(intent: Intent?) { val deepLinkResolver = factory.getResolver(DeepLinkType.from(uri)) deepLinkResolver?.resolve(uri) } // Factoryの実体 override fun getResolver(type: DeepLinkType): DeepLinkResolver? = when (type) { DeepLinkType. MERPAY_DEEP_LINK -> MerpayDeepLinkResolver( context) }
  46. 以前のスタイル class DeepLinkActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) presenter.handleDeepLink( intent) } // Presenterの実体 override fun handleDeepLink (intent: Intent?) { val deepLinkResolver = factory.getResolver(DeepLinkType.from(uri)) deepLinkResolver?.resolve(uri) } // Factoryの実体 override fun getResolver(type: DeepLinkType): DeepLinkResolver? = when (type) { DeepLinkType.MERPAY_DEEP_LINK -> MerpayDeepLinkResolver(context) }
  47. 以前のスタイル class DeepLinkActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?)

    { super.onCreate(savedInstanceState) presenter.handleDeepLink( intent) } // Presenterの実体 override fun handleDeepLink (intent: Intent?) { val deepLinkResolver = factory.getResolver(DeepLinkType.from(uri)) deepLinkResolver?.resolve(uri) } // Factoryの実体 override fun getResolver(type: DeepLinkType): DeepLinkResolver? = when (type) { DeepLinkType.MERPAY_DEEP_LINK -> MerpayDeepLinkResolver(context) } このClassで ・タブをメルペイに変える ・別Activityを起動する の2つが行われていた
  48. 新しいスタイル navigation(...) { composable( route = SAMPLE_ROUTE, deepLinks = listOf(

    navDeepLink { uriPattern = /* URL Patterns */ } ) ) {...} }
  49. 新しいスタイル navigation(...) { composable( route = SAMPLE_ROUTE, deepLinks = listOf(

    navDeepLink { uriPattern = /* URL Patterns */ } ) ) {...} } Navigation ComponentのDeep Links
  50. メルペイのDeep Linkをここで使う には…

  51. 新しいスタイル composable( route = SAMPLE_ROUTE, deepLinks = listOf( navDeepLink {

    uriPattern = /* URL Patterns */ } ) ) {...}
  52. 新しいスタイル val merpayDeepLinks = MerpayDeepLink.values() .map { navDeepLink { uriPattern

    = it.uri } } composable( route = SAMPLE_ROUTE, deepLinks = merpayDeepLinks + listOf( navDeepLink { uriPattern = /* URL Patterns */ } ) ) {...}
  53. 新しいスタイル val merpayDeepLinks = ... composable( route = SAMPLE_ROUTE, deepLinks

    = merpayDeepLinks + listOf( navDeepLink { uriPattern = /* URL Patterns */ } ) ) {...}
  54. 新しいスタイル val merpayDeepLinks = ... composable( route = SAMPLE_ROUTE, deepLinks

    = merpayDeepLinks + listOf( navDeepLink { uriPattern = /* URL Patterns */ } ) ) { val merpayDeepLinkUri = it.arguments?.getParcelable<Intent>(NavController.KEY_DEEP_LINK_INTENT)?.data LaunchedEffect(Unit) { if (merpayDeepLinkUri != null) { // Deep Linkを処理 } }
  55. できた。

  56. Q4. どの画面からやっていこう?

  57. 新メルカリアプリ(再掲) メルペイ 新メルカリ 新Design System Submodule Design System

  58. 新メルカリアプリ(再掲) メルペイ Design System 新メルカリ 新Design System Submodule Composable Android

    View
  59. こうなる メルペイ Design System 新メルカリ 新Design System Submodule 新メルペイ

  60. 参照関係 メルペイ Design System 新メルカリ 新Design System Submodule 新メルペイ 見える

    見えない
  61. 画面遷移の根っこがよさそう?

  62. 他にも… メルペイ Design System 新メルカリ 新Design System Submodule 新メルペイ Design

    Systemは見た目が変わっている箇所もあるた め、できるだけ新・旧の行き来は避けたい
  63. 画面遷移の根っこがよさそう(再)

  64. 君(メルペイタブ)だわ

  65. 他の理由 • 他のタブはすべてJetpack Compose & 新Design System • UIリニューアルが予定されていて、大きく作り直す必要がある メルペイタブの画面からやっていく事に

  66. 03 導入の結果 & 今後

  67. 大変だったところ

  68. パフォーマンスまわり難しい

  69. パフォーマンスチューニングの必要性 • 「こうやっていれば大丈夫」という過去の経験・勘所が使えない ◦ やってみなければ分からない事が多い • 問題があった場合に、それがアプリ側の使い方の問題なのか、Jetpack Composeの潜在課題なのかの都度切り分けが必要となる

  70. 慣れるの大変

  71. 新しい作法 • rememberなどComposable特有の作法や考え方に慣れるまでは大 変でした ◦ 頻繁にペアプロ・技術的雑談等をすることで、短い期間で慣れること ができた

  72. 良かったところ

  73. xmlより作りやすい

  74. xmlよりやりやすい点 1 • .xmlと.ktの行ったり来たりが不要で、一箇所で該当要素のすべてを見れ る

  75. xmlよりやりやすい点 2 • Stateによる出し分けのあるUIがつくりやすい ◦ xml ▪ すべてのUIをつくり、visibilityを切り替える等 ◦ Jetpack

    Compose ▪ Stateによる分岐の中でそれぞれのUIを記述できる ※画面は開発中のものです 例:
  76. xmlよりやりやすい点 2 • Stateによる出し分けのあるUIがつくりやすい ◦ xml ▪ すべてのUIをつくり、visibilityを切り替える等 ◦ Jetpack

    Compose ▪ Stateによる分岐の中でそれぞれのUIを記述できる ※画面は開発中のものです ここの出し分け 例:
  77. xmlよりやりやすい点 3 • UIコンポーネントに可視性を設定できる ◦ layout の xml はモジュール内専用かモジュール外からもアクセス して良いのかがわかりにくい

    ◦ Composable 関数なら internal 等の可視性指定が可能 @Composable internal fun InternalSampleListItem (){}
  78. データ監視のコード書かなくていい

  79. データフロー • Stateの変化は常に監視され、変更されるとRecomposeされる ◦ その変化をUIに反映させるためのコードをわざわざ書かなくて良い ◦ Lifecycleを気にするケースも少ない

  80. ライブラリが良かった

  81. ライブラリ • Accompanistにあるライブラリがかゆいところに手が届いた ◦ Pager Layout ▪ Content Padding &

    Item Scroll Effect ▪ Indicators ▪ PagerState ※画面は開発中のものです 例:
  82. 今後

  83. 今後やっていきたいこと • さらなるJetpack Compose化 ◦ 方針パターンをまず議論 ▪ モノレポ化後に… • 大きな変更のある画面から段階的に

    • 一定期間を取って全画面一気に書き換えていく • メルペイ内遷移のNavigation Component化 ◦ 脱Conductor
  84. conductor.Controller import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return ComposeView(container. context).apply { setContent { ControllerRoot { // <-- Themeなどの指定をまとめたもの SampleScreen( viewModel .inputs, viewModel.state.collectAsState() ) }
  85. conductor.Controller import com.bluelinelabs.conductor.Controller; class SampleController : Controller() { private val

    viewModel = SampleViewModel(SampleDataSourceImpl()) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View { return ComposeView(container. context).apply { setContent { ControllerRoot { // <-- Themeなどの指定をまとめたもの SampleScreen( viewModel .inputs, viewModel.state.collectAsState() ) }
  86. Conductorによる画面遷移 • 単一のActivity上でRouterによってController群を切り替えることで 画面遷移を実現している。 Activity Controller Toolbar Router

  87. Conductorによる画面遷移 • 単一のActivity上でRouterによってController群を切り替えることで 画面遷移を実現している。 Activity Controller ≒ Fragment, Composable etc

    Toolbar Router ≒ NavController
  88. Conductorによる画面遷移 • 単一のActivity上でRouterによってController群を切り替えることで 画面遷移を実現している。 Jetpackで同等機能が実現されているものは、そちらに寄せておく方が今後 様々なメリットを享受しやすい Activity Controller ≒ Fragment,

    Composable etc Toolbar Router ≒ NavController
  89. ご清聴ありがとうございました