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

Android at Scale @Square

7cfefc4ecbffbe84b59de233d3fa4645?s=47 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.

7cfefc4ecbffbe84b59de233d3fa4645?s=128

Ralf

November 25, 2019
Tweet

Transcript

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

  2. :app

  3. :app

  4. :app1 :app2

  5. :lib1 :app2 :app1

  6. :app1 :app2

  7. :lib1 :app2 :app1

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

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

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

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

  12. None
  13. :hairball :app2 :app1 :lib1 :lib2 Number of modules and lines

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

    of modules and lines of code grow
  15. • We have a mono repository Mono or multi repository?

  16. Number of modules and lines of code grow

  17. Number of modules and lines of code grow

  18. Number of modules and lines of code grow

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

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

    matters
  21. • Logical unit to perform work • Self-contained piece of

    the whole system • Loose coupling • High cohesion What is a module in modular design?
  22. • Code is easier portable • Code is more reliable

    • Code is easier to test • Code is easier to maintain • ... Advantages of modular design?
  23. class Feature(b: LoginStrategy) class PosLoginStrategy() Dependency inversion

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

    inversion
  25. • With multiple products? ◦ Yes!!! • With multiple modules?

    ◦ Yes!! • With a single module? ◦ Yes! • Without tests? ◦ ¯\_(ツ)_/¯ Do we need dependency inversion?
  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
  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
  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
  29. Dependencies class Feature(b: LoginStrategy) interface LoginStrategy class PosLoginStrategy() : LoginStrategy

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

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

  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
  33. Module Structure :public

  34. Module Structure :public :impl

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

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

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

  38. Module Structure

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

    X
  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!
  41. Module Structure API Implementation Wiring

  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
  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
  44. dependencies { api project(':login-strategy:public') } Module Structure

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

  46. Dependency hierarchy :apps :shared

  47. Module Structure

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

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

  50. Development Apps

  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
  52. Workflows https://square.github.io/workflow/

  53. :account-screen:public interface AccountScreenWorkflow : Workflow<Unit, Unit, LayeredScreen<PosLayering>>

  54. :login-screen:impl class SampleLoginScreenWorkflow @Inject constructor( private val accountScreenWorkflow: Provider<AccountScreenWorkflow> )

    : LoginScreenWorkflow, StatefulWorkflow<Unit, LoginScreenState, Unit, LayeredScreen<PosLayering>>() { override fun render( props: Unit, state: LoginScreenState, context: RenderContext<LoginScreenState, Unit> ): LayeredScreen<PosLayering> { return when (state) { is RenderAccountScreen -> { context.renderChild(accountScreenWorkflow.get(), Unit) { leaveAccountScreen } } } } }
  55. :account-screen:fake class FakeAccountScreenWorkflow @Inject constructor() : AccountScreenWorkflow, StatelessWorkflow<Unit, Unit, LayeredScreen<PosLayering>>()

    { // ... }
  56. :login-screen:demo android { defaultConfig { applicationId "com.squareup.sample.login.screen.demo" } } dependencies

    { // … implementation project(':samples:account-screen-sample:account-screen:fake-wiring') }
  57. Dependency Graph :camera-screen :profile-photo-screen :edit-account-screen :account-screen:impl :account-screen:public

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

  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
  60. Build times (in seconds)

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

  62. Benchmark build times

  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
  64. Q&A

  65. Ralf Wondratschek @vRallev