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

Forging the path from monolith to multi-module app | droidcon Lisbon

Forging the path from monolith to multi-module app | droidcon Lisbon

Going from a monolith to a multi-module project is a long process, that requires careful planning and a well-thought strategy, especially in medium-large teams.

The TIER application is evolving to a multi-module structure and this talk will be the logbook of the journey, covering the approach that we followed and the philosophy behind the choices that we’ve made with both the mobile platforms in mind.

I will tell you how to balance the entire process without harming the regular feature delivery schedule, where to start modularizing and improvements to the build tools to make life less miserable.

Marco Gomiero

September 28, 2023
Tweet

More Decks by Marco Gomiero

Other Decks in Programming

Transcript

  1. @marcoGomier Marco Gomiero 👨💻 Senior Android Engineer @ TIER 


    Google Developer Expert for Kotlin Forging the path from monolith to multi-module app
  2. @marcoGomier 🧑💻 👩💻 👨💻 👨💻 👨💻 🧑💻 👩💻 👩💻 👨💻

    👩💻 👨💻 👨💻 👨💻 👨💻 👩💻 👩💻
  3. @marcoGomier 🧑💻 👩💻 👨💻 👨💻 👨💻 🧑💻 👩💻 👩💻 👨💻

    👩💻 👨💻 👨💻 👨💻 👨💻 👩💻 👩💻
  4. @marcoGomier 🧩 Reusable modules 🤓 Simplify development 🧑💻 Split responsibilities

    & clarify ownership 🚀 Improved build time Modularization in a nutshell
  5. :shell :features:feat1 :features:feat1-contract :libraries:lib1 :libraries:lib2 :features :libraries :libraries:lib3 TIER module

    layers :clients :app :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2 :clients:client2-mock :clients:client2-contract :features:feat2 :features:feat2-contract
  6. :shell :features:feat1 :features:feat1-contract :libraries:lib1 :libraries:lib2 :features :libraries :libraries:lib3 TIER module

    layers :clients :app :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2 :clients:client2-mock :clients:client2-contract :features:feat2 :features:feat2-contract
  7. @marcoGomier Shell • Shared core common code • Should not

    be a new monolith with a different name 😅 • Not depend on upper layers :shell
  8. :shell :features:feat1 :features:feat1-contract :libraries:lib1 :libraries:lib2 :features :libraries :libraries:lib3 TIER module

    layers :app :features:feat2 :features:feat2-contract :clients :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2 :clients:client2-mock :clients:client2-contract
  9. @marcoGomier :clients:client1 :clients:client1-mock :clients:client1-contract Clients • Encapsulate the usage of

    external 
 libraries and dependencies 
 • Every client has a contract, an implementation and a mock module
  10. @marcoGomier Client: contract • A contract for the outside world

    • Contains Interfaces and Data Classes • Can depend on shell @marcoGomier :clients:locationprovider-contract interface LocationProvider { fun getUserLocation(): UserLocation } data class UserLocation( val latitude: Double, val longitude: Double, ) :clients:client1-contract
  11. @marcoGomier Client: implementation :clients:client1 • The implementation of the contract

    • Just an abstraction without any business logic • Has a dependency on its contract • Can depend on shell • Invisible for other modules (except for :app) @marcoGomier :clients:locationprovider internal class LocationProviderImpl( ... ): LocationProvider { override fun getUserLocation(): UserLocation { // Return a location from the GPS } }
  12. @marcoGomier Client: mock :clients:client2-mock @marcoGomier :clients:locationprovider-mock internal class MockLocationProvider( ...

    ): LocationProvider { override fun getUserLocation(): UserLocation { // Return a location from a mock provider } } • An implementation of the contract that returns fake data • Has a dependency on its contract • Can depend on shell • Invisible for other modules (except for :app)
  13. :shell :features:feat1 :features:feat1-contract :features TIER module layers :app :features:feat2 :features:feat2-contract

    :clients :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2 :clients:client2-mock :clients:client2-contract :libraries:lib1 :libraries:lib2 :libraries :libraries:lib3
  14. :shell TIER module layers :app :clients :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2

    :clients:client2-mock :clients:client2-contract :libraries:lib1 :libraries:lib2 :libraries :libraries:lib3 :features:feat1 :features:feat1-contract :features :features:feat2 :features:feat2-contract
  15. @marcoGomier Features • A specific feature, could be user-facing or

    not • Every feature has a contract and 
 an implementation module :features:feat1 :features:feat1-contract
  16. @marcoGomier Feature: contract • A contract for the outside world

    • What a feature can do and how it can be launched • Contains Interfaces and Data Classes • Can depend on libraries, clients or shell @marcoGomier :features:location-contract interface LocationUseCase { fun getLocation(): Observable<Location> fun getLatestLocation(): Single<Location> fun fetchLocation(): Single<Location> } data class Location( val latitude: Double, val longitude: Double, ) :features:feat1-contract
  17. @marcoGomier Feature: implementation :features:feat1 • The implementation of the contract

    • Has a dependency on its contract • Can depend on other feature contracts, libraries, clients or shell • Invisible for other modules (except for :app) @marcoGomier :features:location internal class LocationUseCaseImpl( ... ) : LocationUseCase { override fun getLocation(): Observable<Location> { ... } 
 ... }
  18. @marcoGomier Contract modules: why? • Low coupling: avoid knowledge of

    inner workings. • Risk of need to import lot of transitive dependencies, when a module depends on many others • Avoid increasing incremental build time
  19. :shell :features:feat1 :features:feat1-contract :libraries:lib1 :libraries:lib2 :features :libraries :libraries:lib3 TIER module

    layers :clients :app :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2 :clients:client2-mock :clients:client2-contract :features:feat2 :features:feat2-contract
  20. @marcoGomier App • App initialization • Dependency Injection setup •

    Link all the module together • It should be as slim as possible :app
  21. :shell :features:feat1 :features:feat1-contract :libraries:lib1 :libraries:lib2 :features :libraries :libraries:lib3 TIER module

    layers :clients :app :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2 :clients:client2-mock :clients:client2-contract :features:feat2 :features:feat2-contract
  22. :shell :libraries:lib1 :libraries:lib2 :features :libraries :libraries:lib3 TIER module layers :clients

    :app :clients:client1 :clients:client1-mock :clients:client1-contract :clients:client2 :clients:client2-mock :clients:client2-contract :features:feat1 :features:feat1-contract :features:feat2 :features:feat2-contract :legacy
  23. @marcoGomier Legacy • A huge module with the non-modularized codebase

    • Extract future features from here • Try to not put any new code in it • Can depend on feature contracts, libraries, clients or shell :legacy
  24. @marcoGomier Starting point • Select a feature to start with

    • Can be a feature with UI or without UI • A non UI feature will be easier: less things to figure out (spoiler: navigation) • Extract Ride Handling to :features:ride
  25. @marcoGomier • Figure out shared dependencies • Move them to

    :shell • Extract the Ride Handling into a new module Start modularizing
  26. @marcoGomier Extract UI Features • Time to extract a UI

    feature • How to navigate between feature modules? • 🚧 ⛔
  27. @marcoGomier Current Navigation Status • Jetpack Navigation • Every “feature”

    has its own navigation graph • Not a single Activity Architecture • Mostly every “feature” has its own hosting Activity
  28. @marcoGomier :features:customersupport CustomerSupport Add a Fragment in the current flow

    ShopActivity ArticleList ArticleDetail CustomerSupport :features:shop
  29. @marcoGomier Some issues • Start a feature from another module

    
 ⛔ Deeplink with a single activity that includes all the graphs • Start a feature and get a result back 
 ⛔ Not out-of-the-box with Jetpack Navigation • Add a Fragment in the current navigation graph 
 ⛔ Not out-of-the-box with Jetpack Navigation
  30. @marcoGomier First idea • Start a feature from another module

    
 💡 Start the Activity from the Contract • Start a feature and get a result back 
 ⛔ Not out-of-the-box with Jetpack Navigation • Add a Fragment in the current navigation graph 
 ⛔ Not out-of-the-box with Jetpack Navigation
  31. @marcoGomier First idea • Start a feature from another module

    
 💡 Start the Activity from the Contract • Start a feature and get a result back 
 💡 Start Activity for result from the Contract • Add a Fragment in the current navigation graph 
 ⛔ Not out-of-the-box with Jetpack Navigation
  32. @marcoGomier First idea • Start a feature from another module

    
 💡 Start the Activity from the Contract • Start a feature and get a result back 
 💡 Start Activity for result from the Contract • Add a Fragment in the current navigation graph 
 💡 Get the Fragment instance from the Contract. Every feature module subcomponent exposes a Router interface that will be implemented by the callers
  33. @marcoGomier First idea: some issues • Lot’s of boilerplate •

    The Activity needs to subscribe on navigation event • Navigating to somewhere depends on the implementation details: Activity, Fragment, Bottom sheet, etc
  34. @marcoGomier Navigation SDK Router Activity, Fragment, Extras, Transitions … Destination

    Destination ViewModel Feature Module Navigator App Module RouteResolver Navigation SDK: how it works Route
  35. @marcoGomier Navigation SDK :features:shop-contract object ShopDestination : Destination :app internal

    object AppRouteResolver { fun resolveRoute(destination: Destination): Route = when (destination) { is ShopDestination -> { fragmentRoute<ShopActivity, ShopHomeScreenFragment>() } else -> { throw IllegalArgumentException( "Router resolver not implemented for destination: $destination", ) } } }
  36. @marcoGomier Navigation SDK :features:smap class MapViewModel( private val router: Router,

    ) : ViewModel() { fun navigateToShop() { router.navigateTo(ShopDestination) } }
  37. @marcoGomier New navigation approach • Everything happens in the ViewModel

    • Less boilerplate to write • The navigation logic can be unit-tested
  38. @marcoGomier Dependency Management • Some version numbers were hardcoded •

    Switch to Gradle Version Catalog for managing dependencies [versions] io-reactivex-rxjava2-rxandroid = "2.1.1" io-reactivex-rxjava2-rxkotlin = "2.4.0" com-jakewharton-rxrelay = "2.1.1" [libraries] com-jakewharton-rxrelay = { module = "com.jakewharton.rxrelay2:rxrelay", version.ref = "com-jakewharton-rxrelay" } io-reactivex-rxjava2-rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "io-reactivex-rxjava2-rxandroid" } io-reactivex-rxjava2-rxkotlin = { module = "io.reactivex.rxjava2:rxkotlin", version.ref = "io-reactivex-rxjava2-rxkotlin" } [bundles] rxjava2 = ["io-reactivex-rxjava2-rxandroid", "io-reactivex-rxjava2-rxkotlin", "com-jakewharton-rxrelay"] @marcoGomier gradle/libs.versions.toml
  39. @marcoGomier Dependency Management • Some version numbers were hardcoded •

    Switch to Gradle Version Catalog for managing dependencies • Bundles are awesome [bundles] feature-common-deps = ["androidx-appcompat", "androidx-constraintlayout", "androidx-recyclerview"] feature-testing = [“junit”, "androidx-io-mockk"] @marcoGomier gradle/libs.versions.toml
  40. @marcoGomier More 🤖 • CookieCutter template to create a new

    feature or library module. • Automatically add custom project configuration
  41. @marcoGomier Restarting point • More utility modules are required for

    more complex UI feature modules: (testing tools, feature-flags, etc) 
 • Now a UI feature can be extracted 🎉
  42. @marcoGomier July 2021 Timeline Nov 2021 Sept 2021 Aug 2021

    :features:ride creation :shell 
 creation Architecture Decisions Feb 2022 May 2022 Apr 2022 March 2022 Ready for bigger UI module More utility modules First UI feature module Gradle improvements • Took almost a year to have everything in place First Navigation discussion
  43. @marcoGomier Timeline July 2021 Nov 2021 Sept 2021 Aug 2021

    :features:ride creation :shell 
 creation Architecture Decisions Feb 2022 May 2022 Apr 2022 March 2022 Ready for bigger UI module More utility modules First UI feature module Gradle improvements First Navigation discussion Jun 2022 New
 Modules
  44. @marcoGomier July 2021 Timeline Nov 2021 Sept 2021 Aug 2021

    First Navigation discussion :features:ride creation :shell 
 creation Architecture Decisions Feb 2022 May 2022 Apr 2022 March 2022 Ready for bigger UI module More utility modules First UI feature module Gradle improvements Jun 2022 Nov 2022 Navigation SDK New
 Modules Now Jan 2023 Dec 2022 More and more modules Clients
 Layer Convention plugins
  45. @marcoGomier Timeline Jun 2022 Nov 2022 Navigation SDK New
 Modules

    Now Jan 2023 Dec 2022 More and more modules Clients
 Layer Convention plugins • More than 160 modules so far 🧩 • Sometime the “best decisionTM” is not the best one
  46. @marcoGomier Conclusions • A long and difficult journey but totally

    worth it • There’s no perfect recipe • Don't be afraid to fail • Don’t rush it • Don’t refactor You will have a chance to find “hidden pearls” of your project 😅
  47. Resources • https://developer.android.com/guide/navigation/navigation-multi-module • https://www.droidcon.com/2019/11/15/android-at-scale-square/ • https://medium.com/inside-aircall/why-did-we-move-away-from-navigation-component- f2160f7c3f4b • https://github.com/slackhq/slack-gradle-plugin

    • https://github.com/cookiecutter/cookiecutter • https://docs.gradle.org/current/userguide/platforms.html • https://github.com/android/nowinandroid/blob/main/docs/ModularizationLearningJourney.md • https://slack.engineering/stabilize-modularize-modernize-scaling-slacks-mobile-codebases/ • https://slack.engineering/stabilize-modularize-modernize-scaling-slacks-mobile-codebases-2/ • https://www.youtube.com/watch?v=K65M6jpyN4Y • https://github.com/android/nowinandroid/tree/main/build-logic • https://developer.squareup.com/blog/herding-elephants/
  48. @marcoGomier Marco Gomiero 👨💻 Senior Android Engineer @ TIER 


    Google Developer Expert for Kotlin Thank you! Twitter: @marcoGomier 
 Github: prof18 
 Website: marcogomiero.com 
 Mastodon: androiddev.social/@marcogom