Slide 1

Slide 1 text

(Unofficial) Guide to Architecture Guide Vol. II Sa-ryong Kang Senior Developer Relations Engineer @ Google

Slide 2

Slide 2 text

Dependency Injection ● Downsides of Hilt ● Hilt on Compose Table of Contents Foreword 1 ● Self-intro ● What the Guide doesn’t say 3 Modular Architecture ● Why and when to modularize ● 6 anti-patterns 2

Slide 3

Slide 3 text

● The opinions stated here are my own, not necessarily those of Google. Disclaimer

Slide 4

Slide 4 text

● Senior Developer Relations Engineer @ Google ○ Mountain View, California ● Working with ○ LINE ○ X (formerly Twitter) ○ Warner Bros Discover (HBO Max) ○ NBC Universal (Peacock TV) Who is this guy?

Slide 5

Slide 5 text

● Fast adoption of latest JDK toolchain (think how long it took for JDK 7 → 8 → 11) ● Activity Embedding for Large Screen devices ● Edge-to-edge layout (will be released soon!) ● Compose Accessibility bug (released last week!) ● Less FCM delay ● Fixed a lot of platform bugs What we could do with top developers

Slide 6

Slide 6 text

● The guide doesn’t intend to cover every patterns and architecture that make sense in mobile design ○ It’s curated collection of best practices and recommendations for most common use cases ● It's not something like Bible or silver bullet, meaning you need to be specific and creative to your own business / technical requirements. What "Google Guide" doesn't say (reprise)

Slide 7

Slide 7 text

● Circuit: MVP on Compose! ○ Dilemma of MVP - Traditionally, View and Presenter calls methods in each other, which Jetpack Compose doesn't allow it Insightful Example - Circuit Ref: https://slackhq.github.io/circuit/

Slide 8

Slide 8 text

● Turo’s architecture (github.com/open-turo/nibel) ○ Creative approach of type-safe, hybrid (Compose Nav + Jetpack Nav) navigation with KSP annotation ○ Take a look with caution ● Redwood by Cashapp (github.com/cashapp/redwood) ● RIBs by Uber (github.com/uber/RIBs) ● Good examples to see how devs can accept architectural principles into opinionated framework ○ However, note that they’re designed to accelerate developer on their unique business requirements. Other Insightful Examples

Slide 9

Slide 9 text

Dependency Injection

Slide 10

Slide 10 text

● We don't repeat any thing written in the Guide ○ Why should I use DI in my project? ○ Basic concepts of Dagger / Hilt ○ How to use Dagger / Hilt ○ Refer to d.android.com/training/dependency-injection What we don't discuss today

Slide 11

Slide 11 text

● Implementation of JSR-330 ○ Binding: @Inject, @Qualifier ○ Scope: @Scope ● DAG (Directed Acyclic Graphs) ○ Binding, Component Dagger class A @Inject constructor(b: B) class B @Inject constructor(c: C, d: D) class C @Inject constructor() class D @Inject constructor() A B D C

Slide 12

Slide 12 text

● Powerful: initially used & tested by Google’s 1st party apps ● Reduces boilerplate ○ Preset Component interfaces → aggregate using @InstallIn ○ Default binding Eg. SavedStateHandle, ApplicationContext ○ Convenient annotations by Gradle Plugin Eg. @AndroidEntryPoint ● Better Testing Why Hilt?

Slide 13

Slide 13 text

● Tricky to extend Component/Scope structure ○ Say you want to add "Account" component / scope ○ before-/after-login, multiple accounts ● Solution: custom scope ○ dagger.dev/hilt/custom-components.html Downside of Dagger / Hilt (1) AccountComponent @AccountScoped

Slide 14

Slide 14 text

● github.com/square/anvil ● A Kotlin compiler plugin (no Java, no KAPT) ○ Same functionalities in Dagger + convenient extensions ● Scope (!= Dagger Scope) ○ Just a marker ○ Aggregate using @MergeComponent, @ContributesTo Noteworthy alternative - Anvil (1)

Slide 15

Slide 15 text

● Whetstone - github.com/deliveryhero/whetstone ○ Extension of Anvil that provide hilt-like structure Noteworthy alternative - Anvil (2)

Slide 16

Slide 16 text

● No native Kotlin support ○ Higher-order functions and default parameters Downside of Dagger / Hilt (2)

Slide 17

Slide 17 text

● Not so Compose friendly ● Structure ○ DAGger - discourages static top-level functions by building an object graph ○ Compose - encourages top-level functions and parameter passing ● State ○ Dagger - encourages statefulness with objects and with scoping ○ Compose - encourages making Composables stateless by state hoisting Downside of Dagger / Hilt (3)

Slide 18

Slide 18 text

@Composable fun Screen(dep1: Dep1, dep2: Dep2, ... ) { Hoge(dep1) Fuga(dep2) } @Composable fun Hoge(dep1: Dep1) { // Call stateless child Composables... } @Composable fun Fuga(dep2: Dep2) { // Call stateless child Composables... }

Slide 19

Slide 19 text

@Composable fun Screen(dep1: Dep1, dep2: Dep2, ... ) { Hoge(dep1) Fuga(dep2) } @Composable fun Hoge(dep1: Dep1) { // Call stateless child Composables... } @Composable fun Fuga(dep2: Dep2) { // Call stateless child Composables... }

Slide 20

Slide 20 text

Why does Screen function need to know about Dep1 and Dep2? Encapsulation Broken!

Slide 21

Slide 21 text

@EntryPoint @InstallIn(ActivityComponent::class) interface ComposeEntryPoint { fun provideDep1(): Dep1 } @Composable internal fun rememberDep1(): Dep1 { val context = LocalContext.current return remember { val entryPoint = EntryPointAccessors.fromActivity( context as Activity, ComposeEntryPoint::class.java ) entryPoint.providesDep1() } }

Slide 22

Slide 22 text

@Composable fun Screen() { Hoge() Fuga() } @Composable fun Hoge(dep1: Dep1 = rememberDep1()) { // ... } @Composable fun Fuga(dep2: Dep2 = rememberDep2()) { // ... }

Slide 23

Slide 23 text

“A CompositionLocal makes sense when it can be potentially used by any descendant, not by a few of them.” ● Not effective for nested structure ● Hard to implement different life-cycle Why don’t we just use CompositionLocal?

Slide 24

Slide 24 text

● Implement stateful composable as a method of State Holder class Composable as a state holder Activity / Fragment Stateless Composable State Holder Composable State Holder Composable Stateless Composable Stateless Composable Stateless Composable Stateless Composable Stateless Composable ...

Slide 25

Slide 25 text

● Similar lifecycle with ViewModelScope ○ It should survive configuration change ● It should be nested Add custom Scope! ComposeComponent @ComposeScoped

Slide 26

Slide 26 text

@ComposeStateHolder // The constructor is injected from the ComposeComponent class HogeComposeStateHolder @Inject internal constructor( private val dep1: Dep1 ) { @Composable fun Hoge() { // ... } }

Slide 27

Slide 27 text

@ComposeStateHolder // The constructor is injected from the ComposeComponent class HogeComposeStateHolder @Inject internal constructor( private val dep1: Dep1 ) { @Composable fun Hoge() { // ... } }

Slide 28

Slide 28 text

Modular Architecture

Slide 29

Slide 29 text

● What is modularization? ● Benefits of modularization ● Common modularization patterns ● Insted, refer to.. ○ d.android.com/topic/modularization ○ マルチモジュールAndroidアプリケーション (DroidKaigi 2019) speakerdeck.com/sansanbuildersbox/multi-module-and roid-application What we don't discuss today

Slide 30

Slide 30 text

● Build times / developer velocity ● Ownership, accountability, and code health ● App responsiveness ● Code sharing Why is modularization good?

Slide 31

Slide 31 text

● If Module A strongly depends on internal behavior of Module B, it means that ... ○ You failed to isolate Module A from Module B’s complexity ○ APIs in Module B should defined well ○ Or, Module B should be included in Module A ● If not, you’re free to seperate them! Principle

Slide 32

Slide 32 text

● ASAP! ○ The effort required to divide the modules is not large. ○ The longer modularization is delayed, the more painful it becomes. ○ Through modularization, previously undiscovered architectural problems can be found and solved, and a well-encapsulated structure can be maintained. ○ Modularization creates a structure that is easier to test. When?

Slide 33

Slide 33 text

● Circular dependencies ○ Refer to the Guide + DroidKaigi 2019 session Anti-pattern (1)

Slide 34

Slide 34 text

● Solution ○ Mediator (as in the Guide) ○ Extract Interface into higher-level module ○ Interface / implementation module pairs Anti-pattern (1)

Slide 35

Slide 35 text

● Inverse dependencies ○ Calling APIs in other module that doesn’t have conceptual dependency → It makes dependency struct counter-intuitive ○ It also harms scalability Anti-pattern (2)

Slide 36

Slide 36 text

● Abstractions should not depend on details. Solution: Dependency Inversion Principle NetworkState Module Receiver DataStore Module LocalData Store Receiver DataStore Module LocalDataStore Impl. LocalData Store Interface NetworkState Module

Slide 37

Slide 37 text

● High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces). Dependency Inversion Principle (2) Data Interface Module Domain Module UseCase Data Module Repository Domain Module UseCase Data Impl Module Repository Impl Repository Interface

Slide 38

Slide 38 text

// in Domain Module class HogeUseCase @Inject internal constructor(repository: FugaRepository) { fun executeHoge() { repository.fuga() } } // in Data Interface Module interface FugaRepository { fun fuga() } // in Data Implementation Module internal class FugaRepositoryImpl : FugaRepository { override fun fuga() { ... } }

Slide 39

Slide 39 text

// domain/build.gradle implementation project(":data-interface") runtimeOnly project(":data-impl") // data-interface/build.gradle // empty // data-impl/build.gradle implementation project(":data-interface") // data-impl/.../di/DataModule.kt @Module @InstallIn(SingletonComponent::class) object DataModule { @Provides fun provideFugaRepository(): FugaRepository = FugaRepositoryImpl() } Let’s set it up! :domain :data-interface :data-impl implementation implementation runtimeOnly

Slide 40

Slide 40 text

// domain/build.gradle implementation project(":data-interface") runtimeOnly project(":data-impl") // data-interface/build.gradle // empty // data-impl/build.gradle implementation project(":data-interface") // data-impl/.../di/DataModule.kt @Module @InstallIn(SingletonComponent::class) object DataModule { @Provides fun provideFugaRepository(): FugaRepository = FugaRepositoryImpl() } Let’s set it up! :domain :data-interface :data-impl implementation implementation runtimeOnly

Slide 41

Slide 41 text

CREDITS: This presentation template was created by Slidesgo, including icons by Flaticon, infographics & images by Freepik and illustrations by Stories Thank you so much! @justfaceit_kr