Slide 1

Slide 1 text

@marcoGomier Marco Gomiero πŸ‘¨πŸ’» Senior Android Engineer @ TIER 
 Google Developer Expert for Kotlin Forging the path from monolith to multi-module app

Slide 2

Slide 2 text

@marcoGomier πŸ§‘πŸ’» πŸ‘©πŸ’»

Slide 3

Slide 3 text

@marcoGomier πŸ§‘πŸ’» πŸ‘©πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ§‘πŸ’» πŸ‘©πŸ’» πŸ‘©πŸ’» πŸ‘¨πŸ’» πŸ‘©πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘©πŸ’» πŸ‘©πŸ’»

Slide 4

Slide 4 text

@marcoGomier πŸ§‘πŸ’» πŸ‘©πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ§‘πŸ’» πŸ‘©πŸ’» πŸ‘©πŸ’» πŸ‘¨πŸ’» πŸ‘©πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘¨πŸ’» πŸ‘©πŸ’» πŸ‘©πŸ’»

Slide 5

Slide 5 text

@marcoGomier 🧩 Reusable modules πŸ€“ Simplify development πŸ§‘πŸ’» Split responsibilities & clarify ownership πŸš€ Improved build time Modularization in a nutshell

Slide 6

Slide 6 text

@marcoGomier There is not a "right way" Disclaimer:

Slide 7

Slide 7 text

@marcoGomier #1 Decide your architecture

Slide 8

Slide 8 text

@marcoGomier Standard module layers App Features Libraries

Slide 9

Slide 9 text

: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

Slide 10

Slide 10 text

: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

Slide 11

Slide 11 text

@marcoGomier Shell ● Shared core common code ● Should not be a new monolith with a different name πŸ˜… ● Not depend on upper layers :shell

Slide 12

Slide 12 text

: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

Slide 13

Slide 13 text

@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

Slide 14

Slide 14 text

@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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

: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

Slide 18

Slide 18 text

@marcoGomier :libraries:lib1 Libraries ● Small reusable components ● Can depend on other libraries, clients or shell

Slide 19

Slide 19 text

: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

Slide 20

Slide 20 text

@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

Slide 21

Slide 21 text

@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 fun getLatestLocation(): Single fun fetchLocation(): Single } data class Location( val latitude: Double, val longitude: Double, ) :features:feat1-contract

Slide 22

Slide 22 text

@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 { ... } 
 ... }

Slide 23

Slide 23 text

@marcoGomier Contract modules: why? :features:feat1-contract :features:feat2 :features:feat2-contract :clients:client1 :clients:client1-contract :features:feat1

Slide 24

Slide 24 text

@marcoGomier Contract modules: why? :features:feat1-contract :features:feat2 :features:feat2-contract :clients:client1 :clients:client1-contract :features:feat1

Slide 25

Slide 25 text

@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

Slide 26

Slide 26 text

: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

Slide 27

Slide 27 text

@marcoGomier App ● App initialization ● Dependency Injection setup ● Link all the module together ● It should be as slim as possible :app

Slide 28

Slide 28 text

: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

Slide 29

Slide 29 text

: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

Slide 30

Slide 30 text

@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

Slide 31

Slide 31 text

@marcoGomier How to enforce this structure? ● Documentation ● β€œComputer says no” πŸ€–

Slide 32

Slide 32 text

@marcoGomier #2 Start doing things

Slide 33

Slide 33 text

@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

Slide 34

Slide 34 text

@marcoGomier Find shared deps β†’ Dependecies Analysis

Slide 35

Slide 35 text

@marcoGomier

Slide 36

Slide 36 text

@marcoGomier ● Figure out shared dependencies ● Move them to :shell ● Extract the Ride Handling into a new module Start modularizing

Slide 37

Slide 37 text

@marcoGomier πŸŽ‰

Slide 38

Slide 38 text

:app :features:ride :features:ride-contract :features :shell

Slide 39

Slide 39 text

:app :features:ride :features:ride-contract :features :shell :legacy

Slide 40

Slide 40 text

@marcoGomier Extract UI Features ● Time to extract a UI feature ● How to navigate between feature modules? ● 🚧 β›”

Slide 41

Slide 41 text

@marcoGomier #3 Rethink Navigation

Slide 42

Slide 42 text

@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

Slide 43

Slide 43 text

@marcoGomier ShopActivity ArticleList ArticleDetail PurchaseSuccess

Slide 44

Slide 44 text

@marcoGomier :features:shop ShopActivity ArticleList ArticleDetail PurchaseSuccess :features:map MapActivity Map ShopActivity Start a feature from another module

Slide 45

Slide 45 text

@marcoGomier :features:map MapActivity Map QRScannerActivity :features:qr-scanner QRScannerActivity Intro Scan Start a feature from another module and get a result back

Slide 46

Slide 46 text

@marcoGomier :features:customersupport CustomerSupport Add a Fragment in the current flow ShopActivity ArticleList ArticleDetail CustomerSupport :features:shop

Slide 47

Slide 47 text

@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

Slide 48

Slide 48 text

@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

Slide 49

Slide 49 text

@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

Slide 50

Slide 50 text

@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

Slide 51

Slide 51 text

@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

Slide 52

Slide 52 text

@marcoGomier πŸ’‘ A β€œNavigation SDK”

Slide 53

Slide 53 text

@marcoGomier https://medium.com/inside-aircall/why-did-we-move-away-from-navigation-component-f2160f7c3f4b

Slide 54

Slide 54 text

@marcoGomier Navigation SDK Router Activity, Fragment, Extras, Transitions … Destination Destination ViewModel Feature Module Navigator App Module RouteResolver Navigation SDK: how it works Route

Slide 55

Slide 55 text

@marcoGomier Navigation SDK :features:shop-contract object ShopDestination : Destination :app internal object AppRouteResolver { fun resolveRoute(destination: Destination): Route = when (destination) { is ShopDestination -> { fragmentRoute() } else -> { throw IllegalArgumentException( "Router resolver not implemented for destination: $destination", ) } } }

Slide 56

Slide 56 text

@marcoGomier Navigation SDK :features:smap class MapViewModel( private val router: Router, ) : ViewModel() { fun navigateToShop() { router.navigateTo(ShopDestination) } }

Slide 57

Slide 57 text

@marcoGomier New navigation approach ● Everything happens in the ViewModel ● Less boilerplate to write ● The navigation logic can be unit-tested

Slide 58

Slide 58 text

@marcoGomier #4 Improve Gradle Setup

Slide 59

Slide 59 text

@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

Slide 60

Slide 60 text

@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

Slide 61

Slide 61 text

@marcoGomier Module Configuration β†’ Convention Plugins plugins { id 'com.tier.app.android.feature' id 'kotlin-android' } dependencies { ... }

Slide 62

Slide 62 text

@marcoGomier Convention Plugins per module layers com.tier.app.android.application com.tier.app.android.feature com.tier.app.android.library com.tier.app.android.client com.tier.app.android.base.library app: features: libraries: clients: shell:

Slide 63

Slide 63 text

@marcoGomier More πŸ€– ● CookieCutter template to create a new feature or library module. ● Automatically add custom project configuration

Slide 64

Slide 64 text

@marcoGomier #5 Restart doing things

Slide 65

Slide 65 text

@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 πŸŽ‰

Slide 66

Slide 66 text

@marcoGomier #6 Conclusions

Slide 67

Slide 67 text

@marcoGomier July 2021 Timeline Architecture Decisions

Slide 68

Slide 68 text

@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 ● Every 6 weeks, 2 weeks of β€œcooldown” β†’ big chunk of planning and work ● Some work happens during the 6 weeks First Navigation discussion

Slide 69

Slide 69 text

@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

Slide 70

Slide 70 text

@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 Apr 2023 Jan 2023 Dec 2022 More and more modules Clients
 Layer Convention plugins

Slide 71

Slide 71 text

@marcoGomier Timeline Jun 2022 Nov 2022 Navigation SDK New
 Modules Apr 2023 Jan 2023 Dec 2022 More and more modules Clients
 Layer Convention plugins ● More than 140 modules so far 🧩 ● Sometime the β€œbest decisionTM” is not the best one

Slide 72

Slide 72 text

@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 πŸ˜…

Slide 73

Slide 73 text

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/

Slide 74

Slide 74 text

@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