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

段階的Jetpack Compose導入 〜メルペイの場合〜 / Phased Impleme...

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. Junya Matsuyama 株式会社メルペイ Android Engineer 兼 Engineering Manager 2021年7月にメルペイに入社 新機能開発等に従事

    それ以前は携帯電話、ソーシャルゲーム、 SNS、ライブ配 信サービス、タクシー配車サービスなど、様々なモバイル 向けのアプリ・サービスを開発
  2. メルカリアプリ メルカリ メルペイ Design System 初期リリースは約9 年前 蓄積した技術的負 債による開発効率 低下が大きな課題

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

    に 新機能開発を止め て、すべて作り直す 事に 事業成長のための 機能開発・改善を継 続 リリースから3年半 技術的負債はそこ まで溜まっていない
  4. 未来のリポジトリ構成 Step 1 メルペイ 新メルカリ 新Design System subtree Design System

    submoduleをやめ、 subtreeで必要なコード だけを履歴ごと取り込む。
  5. 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反映 }
  6. 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反映 }
  7. 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反映 }
  8. 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, ) }
  9. 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, ) }
  10. 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, ) }
  11. After @Composable fun SampleScreen( inputs: SampleInputs, state: SampleState, ) {

    when (state) { // 画面要素出し分け is Xxx -> { ... } is Yyy -> { SomeComponent( inputs::someEvent) } } }
  12. Action : Before sealed interface SampleAction : Action { object

    SomeAction : SampleAction object AnyAction : SampleAction object HogeAction : SampleAction object PiyoAction : SampleAction object NavigateToHogeAction : SampleAction object NavigateToPiyoAction : SampleAction }
  13. 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 }
  14. 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(...)
  15. 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(...)
  16. 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(...)
  17. 以前のスタイル 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) }
  18. 以前のスタイル 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) }
  19. 以前のスタイル 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) }
  20. 以前のスタイル 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) }
  21. 以前のスタイル 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つが行われていた
  22. 新しいスタイル navigation(...) { composable( route = SAMPLE_ROUTE, deepLinks = listOf(

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

    navDeepLink { uriPattern = /* URL Patterns */ } ) ) {...} } Navigation ComponentのDeep Links
  24. 新しいスタイル val merpayDeepLinks = MerpayDeepLink.values() .map { navDeepLink { uriPattern

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

    = merpayDeepLinks + listOf( navDeepLink { uriPattern = /* URL Patterns */ } ) ) {...}
  26. 新しいスタイル 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を処理 } }
  27. 他にも… メルペイ Design System 新メルカリ 新Design System Submodule 新メルペイ Design

    Systemは見た目が変わっている箇所もあるた め、できるだけ新・旧の行き来は避けたい
  28. xmlよりやりやすい点 2 • Stateによる出し分けのあるUIがつくりやすい ◦ xml ▪ すべてのUIをつくり、visibilityを切り替える等 ◦ Jetpack

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

    Compose ▪ Stateによる分岐の中でそれぞれのUIを記述できる ※画面は開発中のものです ここの出し分け 例:
  30. ライブラリ • Accompanistにあるライブラリがかゆいところに手が届いた ◦ Pager Layout ▪ Content Padding &

    Item Scroll Effect ▪ Indicators ▪ PagerState ※画面は開発中のものです 例:
  31. 今後やっていきたいこと • さらなるJetpack Compose化 ◦ 方針パターンをまず議論 ▪ モノレポ化後に… • 大きな変更のある画面から段階的に

    • 一定期間を取って全画面一気に書き換えていく • メルペイ内遷移のNavigation Component化 ◦ 脱Conductor
  32. 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() ) }
  33. 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() ) }