Share
Embed
(Unofficial) Guide to App Architecture Guide Vol. 2 - DroidKaigi 2023
Covering DI & modular architecture
Sa-ryong Kang
October 05, 2023
Transcript
41 slides
-
(Unofficial) Guide to Architecture Guide Vol. II Sa-ryong Kang Senior
Developer Relations Engineer @ Google -
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 -
• The opinions stated here are my own, not necessarily
those of Google. Disclaimer -
• 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? -
• 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 -
• 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) -
• 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/ -
• 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 -
• 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 -
• 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 -
• 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? -
• 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 -
• 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) -
• Whetstone - github.com/deliveryhero/whetstone ◦ Extension of Anvil that provide
hilt-like structure Noteworthy alternative - Anvil (2) -
• No native Kotlin support ◦ Higher-order functions and default
parameters Downside of Dagger / Hilt (2) -
• 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) -
@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... } -
@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... } -
Why does Screen function need to know about Dep1 and
Dep2? Encapsulation Broken! -
@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() } } -
@Composable fun Screen() { Hoge() Fuga() } @Composable fun Hoge(dep1:
Dep1 = rememberDep1()) { // ... } @Composable fun Fuga(dep2: Dep2 = rememberDep2()) { // ... } -
“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? -
• 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 ... -
• Similar lifecycle with ViewModelScope ◦ It should survive configuration
change • It should be nested Add custom Scope! ComposeComponent @ComposeScoped -
@ComposeStateHolder // The constructor is injected from the ComposeComponent class
HogeComposeStateHolder @Inject internal constructor( private val dep1: Dep1 ) { @Composable fun Hoge() { // ... } } -
@ComposeStateHolder // The constructor is injected from the ComposeComponent class
HogeComposeStateHolder @Inject internal constructor( private val dep1: Dep1 ) { @Composable fun Hoge() { // ... } } -
• 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 -
• Build times / developer velocity • Ownership, accountability, and
code health • App responsiveness • Code sharing Why is modularization good? -
• 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 -
• 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? -
• Circular dependencies ◦ Refer to the Guide + DroidKaigi
2019 session Anti-pattern (1) -
• Solution ◦ Mediator (as in the Guide) ◦ Extract
Interface into higher-level module ◦ Interface / implementation module pairs Anti-pattern (1) -
• 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) -
• 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 -
• 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 -
// 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() { ... } } -
// 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 -
// 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 -
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