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

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

mercari
August 25, 2022

段階的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

August 25, 2022
Tweet

More Decks by mercari

Other Decks in Technology

Transcript

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

    View full-size slide

  2. Junya Matsuyama
    株式会社メルペイ
    Android Engineer 兼 Engineering Manager
    2021年7月にメルペイに入社
    新機能開発等に従事
    それ以前は携帯電話、ソーシャルゲーム、
    SNS、ライブ配
    信サービス、タクシー配車サービスなど、様々なモバイル
    向けのアプリ・サービスを開発

    View full-size slide

  3. Agenda
    01 メルカリアプリの構造
    02 Jetpack Composeの導入
    03 導入の結果 & 今後

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    リリースから3年半
    技術的負債はそこ
    まで溜まっていない

    View full-size slide

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

    リリースから3年半
    技術的負債はそこ
    まで溜まっていない
    新機能開発を止め
    て、すべて作り直す
    事に

    View full-size slide

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

    新機能開発を止め
    て、すべて作り直す
    事に
    事業成長のための
    機能開発・改善を継

    リリースから3年半
    技術的負債はそこ
    まで溜まっていない

    View full-size slide

  10. じゃあどうする?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  17. 未来のリポジトリ構成 最終予定図
    メルペイ
    新メルカリ
    新Design System Design System

    View full-size slide

  18. 02
    Jetpack Composeの導入

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  23. あれ、Jetpack Composeって…

    View full-size slide

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

    View full-size slide

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

    State
    Event
    Unidirectional
    Data Flow

    View full-size slide

  26. 試してみよう

    View full-size slide

  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反映
    }

    View full-size slide

  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反映
    }

    View full-size slide

  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反映
    }

    View full-size slide

  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,
    )
    }

    View full-size slide

  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,
    )
    }

    View full-size slide

  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,
    )
    }

    View full-size slide

  33. After
    @Composable
    fun SampleScreen(
    inputs: SampleInputs,
    state: SampleState,
    ) {
    when (state) {
    // 画面要素出し分け
    is Xxx -> { ... }
    is Yyy -> {
    SomeComponent( inputs::someEvent)
    }
    }
    }

    View full-size slide

  34. UI部分だけのJetpack
    Compose化はできそう。

    View full-size slide

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

    View full-size slide

  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
    }

    View full-size slide

  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
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  41. 遷移もいけそう

    View full-size slide

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

    View full-size slide

  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)
    }

    View full-size slide

  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)
    }

    View full-size slide

  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)
    }

    View full-size slide

  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)
    }

    View full-size slide

  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つが行われていた

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. メルペイのDeep Linkをここで使う
    には…

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. 新しいスタイル
    val merpayDeepLinks = ...
    composable(
    route = SAMPLE_ROUTE,
    deepLinks = merpayDeepLinks +
    listOf(
    navDeepLink { uriPattern = /* URL Patterns */ }
    )
    ) {
    val merpayDeepLinkUri =
    it.arguments?.getParcelable(NavController.KEY_DEEP_LINK_INTENT)?.data
    LaunchedEffect(Unit) {
    if (merpayDeepLinkUri != null) {
    // Deep Linkを処理
    }
    }

    View full-size slide

  55. できた。

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  62. 他にも…
    メルペイ
    Design System
    新メルカリ
    新Design System
    Submodule
    新メルペイ
    Design Systemは見た目が変わっている箇所もあるた
    め、できるだけ新・旧の行き来は避けたい

    View full-size slide

  63. 画面遷移の根っこがよさそう(再)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  66. 03
    導入の結果 & 今後

    View full-size slide

  67. 大変だったところ

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  70. 慣れるの大変

    View full-size slide

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

    View full-size slide

  72. 良かったところ

    View full-size slide

  73. xmlより作りやすい

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  77. xmlよりやりやすい点 3
    ● UIコンポーネントに可視性を設定できる
    ○ layout の xml はモジュール内専用かモジュール外からもアクセス
    して良いのかがわかりにくい
    ○ Composable 関数なら internal 等の可視性指定が可能
    @Composable
    internal fun InternalSampleListItem (){}

    View full-size slide

  78. データ監視のコード書かなくていい

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  81. ライブラリ
    ● Accompanistにあるライブラリがかゆいところに手が届いた
    ○ Pager Layout
    ■ Content Padding & Item Scroll Effect
    ■ Indicators
    ■ PagerState
    ※画面は開発中のものです
    例:

    View full-size slide

  82. 今後やっていきたいこと
    ● さらなるJetpack Compose化
    ○ 方針パターンをまず議論
    ■ モノレポ化後に…
    ● 大きな変更のある画面から段階的に
    ● 一定期間を取って全画面一気に書き換えていく
    ● メルペイ内遷移のNavigation Component化
    ○ 脱Conductor

    View full-size slide

  83. 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()
    )
    }

    View full-size slide

  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()
    )
    }

    View full-size slide

  85. Conductorによる画面遷移
    ● 単一のActivity上でRouterによってController群を切り替えることで
    画面遷移を実現している。
    Activity
    Controller
    Toolbar
    Router

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  88. ご清聴ありがとうございました

    View full-size slide