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

(Unofficial) Guide to App Architecture Guide Vol. 2 - DroidKaigi 2023

(Unofficial) Guide to App Architecture Guide Vol. 2 - DroidKaigi 2023

Covering DI & modular architecture

Sa-ryong Kang

October 05, 2023
Tweet

More Decks by Sa-ryong Kang

Other Decks in Programming

Transcript

  1. 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
  2. • 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?
  3. • 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
  4. • 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)
  5. • 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/
  6. • 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
  7. • 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
  8. • 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
  9. • 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?
  10. • 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
  11. • 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)
  12. • Whetstone - github.com/deliveryhero/whetstone ◦ Extension of Anvil that provide

    hilt-like structure Noteworthy alternative - Anvil (2)
  13. • 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)
  14. @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... }
  15. @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... }
  16. @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() } }
  17. @Composable fun Screen() { Hoge() Fuga() } @Composable fun Hoge(dep1:

    Dep1 = rememberDep1()) { // ... } @Composable fun Fuga(dep2: Dep2 = rememberDep2()) { // ... }
  18. “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?
  19. • 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 ...
  20. • Similar lifecycle with ViewModelScope ◦ It should survive configuration

    change • It should be nested Add custom Scope! ComposeComponent @ComposeScoped
  21. @ComposeStateHolder // The constructor is injected from the ComposeComponent class

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

    HogeComposeStateHolder @Inject internal constructor( private val dep1: Dep1 ) { @Composable fun Hoge() { // ... } }
  23. • 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
  24. • Build times / developer velocity • Ownership, accountability, and

    code health • App responsiveness • Code sharing Why is modularization good?
  25. • 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
  26. • 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?
  27. • Solution ◦ Mediator (as in the Guide) ◦ Extract

    Interface into higher-level module ◦ Interface / implementation module pairs Anti-pattern (1)
  28. • 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)
  29. • 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
  30. • 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
  31. // 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() { ... } }
  32. // 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
  33. // 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
  34. 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