Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

01 メルカリアプリの構造

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

じゃあどうする?

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

02 Jetpack Composeの導入

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

あれ、Jetpack Composeって…

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

試してみよう

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Action : Before sealed interface SampleAction : Action { object SomeAction : SampleAction object AnyAction : SampleAction object HogeAction : SampleAction object PiyoAction : SampleAction object NavigateToHogeAction : SampleAction object NavigateToPiyoAction : SampleAction }

Slide 37

Slide 37 text

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 }

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

遷移もいけそう

Slide 42

Slide 42 text

Q3. Deep Linkはどうしよう

Slide 43

Slide 43 text

以前のスタイル 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) }

Slide 44

Slide 44 text

以前のスタイル 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) }

Slide 45

Slide 45 text

以前のスタイル 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) }

Slide 46

Slide 46 text

以前のスタイル 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) }

Slide 47

Slide 47 text

以前のスタイル 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つが行われていた

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

新しいスタイル 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を処理 } }

Slide 55

Slide 55 text

できた。

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

君(メルペイタブ)だわ

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

03 導入の結果 & 今後

Slide 67

Slide 67 text

大変だったところ

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

慣れるの大変

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

良かったところ

Slide 73

Slide 73 text

xmlより作りやすい

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

ライブラリが良かった

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

今後

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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