$30 off During Our Annual Pro Sale. View Details »

Android at Scale @Square

Ralf
November 25, 2019

Android at Scale @Square

Codebases naturally grow over time by adding new features, abstractions and migrating code to new architectures. We introduce layers to hide implementation details and separate concerns. Good modularization brings many benefits such as better reusability, shorter build times and code isolation.

Square builds payment, software and hardware systems that help businesses of any type. The Android Point of Sale repository faces challenges similar to other large codebases. This talk gives an overview of how the repository evolved over the years, the difficulties we encountered in the recent months and how we addressed them with clear structures and common patterns to keep up with the ongoing growth.

Ralf

November 25, 2019
Tweet

More Decks by Ralf

Other Decks in Programming

Transcript

  1. November 25, 2019
    Android at Scale @Square
    Ralf Wondratschek
    @vRallev

    View Slide

  2. :app

    View Slide

  3. :app

    View Slide

  4. :app1 :app2

    View Slide

  5. :lib1
    :app2
    :app1

    View Slide

  6. :app1 :app2

    View Slide

  7. :lib1
    :app2
    :app1

    View Slide

  8. :hairball
    :app2
    :app1
    :lib1 :lib2

    View Slide

  9. Dependency hierarchy
    :apps
    :feature
    :common
    :api

    View Slide

  10. ● Login?
    ● Checkout?
    ● Settings?
    ● Hairball?
    Dependency hierarchy

    View Slide

  11. Dependency hierarchy
    :apps
    :hairball-feature
    :hairball
    :feature
    :common
    :api
    :delegates

    View Slide

  12. View Slide

  13. :hairball
    :app2
    :app1
    :lib1 :lib2
    Number of modules and lines of code grow

    View Slide

  14. :hairball
    :app2
    :app1
    :lib1 :lib2
    :lib3
    :lib4
    :lib5
    :lib6
    Number of modules and lines of code grow

    View Slide

  15. ● We have a mono repository
    Mono or multi repository?

    View Slide

  16. Number of modules and lines of code grow

    View Slide

  17. Number of modules and lines of code grow

    View Slide

  18. Number of modules and lines of code grow

    View Slide

  19. :hairball
    :app2
    :app1
    :lib1 :lib2
    :lib3
    :lib4
    :lib5
    :lib6
    Weight matters

    View Slide

  20. :hairball
    :app2
    :app1
    :lib1
    :lib2
    :lib3
    :lib4
    :lib5
    :lib6
    Weight matters

    View Slide

  21. ● Logical unit to perform work
    ● Self-contained piece of the whole system
    ● Loose coupling
    ● High cohesion
    What is a module in modular design?

    View Slide

  22. ● Code is easier portable
    ● Code is more reliable
    ● Code is easier to test
    ● Code is easier to maintain
    ● ...
    Advantages of modular design?

    View Slide

  23. class Feature(b: LoginStrategy) class PosLoginStrategy()
    Dependency inversion

    View Slide

  24. class Feature(b: LoginStrategy) interface LoginStrategy
    class PosLoginStrategy() : LoginStrategy
    Dependency inversion

    View Slide

  25. ● With multiple products?
    ○ Yes!!!
    ● With multiple modules?
    ○ Yes!!
    ● With a single module?
    ○ Yes!
    ● Without tests?
    ○ ¯\_(ツ)_/¯
    Do we need dependency inversion?

    View Slide

  26. interface LoginStrategy {
    fun login()
    }
    class PosLoginStrategy @Inject constructor(
    private val helper: Helper
    ) : LoginStrategy {
    override fun login() { }
    @dagger.Module
    abstract class Module {
    @Binds abstract fun bindLoginStrategy(strategy: PosLoginStrategy): LoginStrategy
    }
    }
    Dependency inversion

    View Slide

  27. class PosLoginStrategy @Inject constructor(
    private val helper: Helper,
    private val helper: Helper1,
    private val helper: Helper2,
    private val helper: Helper3,
    private val helper: Helper4,
    private val helper: Helper5,
    private val helper: Helper6,
    private val helper: Helper7,
    private val helper: Helper8
    ) : LoginStrategy
    Dependencies

    View Slide

  28. dependencies {
    api project(':helper1')
    api project(':helper2')
    api project(':helper3')
    api project(':helper4')
    api project(':helper5')
    api project(':helper6')
    api project(':helper7')
    api project(':helper8')
    }
    Dependencies

    View Slide

  29. Dependencies
    class Feature(b: LoginStrategy) interface LoginStrategy
    class PosLoginStrategy() : LoginStrategy

    View Slide

  30. Dependencies
    class Feature(b: LoginStrategy) interface LoginStrategy

    View Slide

  31. dependencies {
    api project(':login-strategy')
    }
    Dependencies for Feature

    View Slide

  32. dependencies {
    api project(':login-strategy')
    api project(':helper1')
    api project(':helper2')
    api project(':helper3')
    api project(':helper4')
    api project(':helper5')
    api project(':helper6')
    api project(':helper7')
    api project(':helper8')
    }
    Dependencies for Feature

    View Slide

  33. Module Structure
    :public

    View Slide

  34. Module Structure
    :public
    :impl

    View Slide

  35. Module Structure
    :public
    :impl
    :impl-wiring

    View Slide

  36. :login-strategy
    Module Structure
    :public
    :impl
    :impl-wiring

    View Slide

  37. Module Structure
    :login-strategy:public
    :login-strategy:impl
    :login-strategy:impl-wiring

    View Slide

  38. Module Structure

    View Slide

  39. :login-strategy
    Module Structure
    :public
    :impl
    :impl-wiring
    :feature
    :public
    :impl
    :impl-wiring
    X

    View Slide

  40. Legacy modules
    ● All the rules of the new module structure don’t apply
    ○ (They’re all the module types in one)
    ● This doesn’t mean you shouldn’t care about dependencies and their size!

    View Slide

  41. Module Structure
    API Implementation
    Wiring

    View Slide

  42. // :public
    interface LoginStrategy {
    fun login()
    }
    // :impl
    class PosLoginStrategy @Inject constructor(
    private val helper: Helper
    ) : LoginStrategy {
    override fun login() { }
    }
    // :impl-wiring
    @dagger.Module
    abstract class LoginStrategyModule {
    @Binds abstract fun bindLoginStrategy(strategy: PosLoginStrategy): LoginStrategy
    }
    Module Structure

    View Slide

  43. dependencies {
    api project(':login-strategy')
    api project(':helper1')
    api project(':helper2')
    api project(':helper3')
    api project(':helper4')
    api project(':helper5')
    api project(':helper6')
    api project(':helper7')
    api project(':helper8')
    }
    Module Structure

    View Slide

  44. dependencies {
    api project(':login-strategy:public')
    }
    Module Structure

    View Slide

  45. Dependency hierarchy
    :apps
    :feature
    :common
    :api

    View Slide

  46. Dependency hierarchy
    :apps
    :shared

    View Slide

  47. Module Structure

    View Slide

  48. Development Apps
    :public
    :impl
    :impl-wiring

    View Slide

  49. Development Apps
    :public
    :impl
    :impl-wiring
    :demo

    View Slide

  50. Development Apps

    View Slide

  51. Development Apps
    ● Install a feature with an isolated application on the device
    ○ Launch the feature directly
    ○ Shorter build times
    ● Replace dependencies by fakes
    ● Isolated UI tests

    View Slide

  52. Workflows
    https://square.github.io/workflow/

    View Slide

  53. :account-screen:public
    interface AccountScreenWorkflow : Workflow>

    View Slide

  54. :login-screen:impl
    class SampleLoginScreenWorkflow @Inject constructor(
    private val accountScreenWorkflow: Provider
    ) :
    LoginScreenWorkflow,
    StatefulWorkflow>() {
    override fun render(
    props: Unit,
    state: LoginScreenState,
    context: RenderContext
    ): LayeredScreen {
    return when (state) {
    is RenderAccountScreen -> {
    context.renderChild(accountScreenWorkflow.get(), Unit) { leaveAccountScreen }
    }
    }
    }
    }

    View Slide

  55. :account-screen:fake
    class FakeAccountScreenWorkflow @Inject constructor() :
    AccountScreenWorkflow,
    StatelessWorkflow>() {
    // ...
    }

    View Slide

  56. :login-screen:demo
    android {
    defaultConfig {
    applicationId "com.squareup.sample.login.screen.demo"
    }
    }
    dependencies {
    // …
    implementation project(':samples:account-screen-sample:account-screen:fake-wiring')
    }

    View Slide

  57. Dependency Graph
    :camera-screen
    :profile-photo-screen
    :edit-account-screen
    :account-screen:impl
    :account-screen:public

    View Slide

  58. Dependency Graph
    :account-screen:public
    :account-screen:fake

    View Slide

  59. Fakes
    ● Reduce size of the dependency graph
    ● Reduce the number of lines of code that need to be compiled
    ● Allow changing behavior as you wish

    View Slide

  60. Build times (in seconds)

    View Slide

  61. Benchmark build times
    ● https://github.com/gradle/gradle-profiler

    View Slide

  62. Benchmark build times

    View Slide

  63. Summary
    ● Square isn’t different, has its own set of unique problems
    ● Modularization is an ongoing process, celebrate as you make progress
    ● Start thinking about dependencies and build time early in the process
    ● Make use of design patterns like dependency inversion

    View Slide

  64. Q&A

    View Slide

  65. Ralf Wondratschek
    @vRallev

    View Slide