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

36・15 Cash App

Benoît Quenaudon
April 26, 2024
160

36・15 Cash App

1300 modules. 50 developers. 40 million customers.
This Android application, what does it look like?
It looks like Cash App.
We have the opportunity to discover, to inspect, to dive indeed into the internal current state of Cash App. No rug unturned, we will reveal it all: tech stack, open-source, APIs, architecture, development process, design system, testing, navigation, CI.
Our Android application is modern, flexible, and reliable. We shall discover how it gets done.

Benoît Quenaudon

April 26, 2024
Tweet

Transcript

  1. interface InvestingAppService { @POST("/2.0/cash/investing/get-discovery") suspend fun getDiscovery( @Body request: GetDiscoveryRequest,

    ): ApiResult<GetDiscoveryResponse> @POST("/2.0/cash/investing/get-customer-settings") fun getCustomerSettings( @Body request: GetCustomerSettingsRequest, ): Single<ApiResult<GetCustomerSettingsResponse>> } Backend
  2. sealed class ApiResult<out T : Any> { data class Success<T

    : Any>(val response: T) : ApiResult<T>() sealed class Failure : ApiResult<Nothing>() { data class NetworkFailure(val error: Throwable) : Failure() data class HttpFailure( val code: Int, val responseHeaderDate: Date? = null, ) : Failure() } } Backend
  3. interface InvestmentEntities { fun discoveryStocks(forSearch: Boolean): Flow<DiscoverySections> fun stockDetails(token: InvestmentEntityToken):

    Flow<StockDetails> } class RealInvestmentEntities @Inject internal constructor( private val cashDatabase: CashDatabase, @Io private val ioDispatcher: CoroutineContext, ) : InvestmentEntities { override fun discoveryStocks( forSearch: Boolean, ): Flow<DiscoverySections> { return cashDatabase.investingDiscoveryQueries .selectDiscoveries(in_search_category = false) .asFlow() .mapToList(ioDispatcher) .f l atMapLatest { entities -> Backend
  4. interface InvestmentEntities { fun discoveryStocks(forSearch: Boolean): Flow<DiscoverySections> fun stockDetails(token: InvestmentEntityToken):

    Flow<StockDetails> } class FakeInvestmentEntities : InvestmentEntities { val discoveryAll = MutableSharedFlow<DiscoverySections>(replay = 1) val discoveryForSearch = MutableSharedFlow<DiscoverySections>(replay = 1) override fun discoveryStocks( forSearch: Boolean, ): Flow<DiscoverySections> { return if (forSearch) discoveryForSearch else discoveryAll } Backend
  5. interface InvestingStateManager { @Composable fun investingStates(): InvestingState } class RealInvestingStateManager

    @Inject constructor( database: CashAccountDatabase, @Io private val ioDispatcher: CoroutineContext, ) : InvestingStateManager { private val stateQueries = database.investingStateQueries @Composable override fun investingStates(): InvestingState { val dbInvestState: Investing_state? by remember { stateQueries.select().asFlow().mapToOne(ioDispatcher) }.collectAsState(null) // Do things imperative style. return investState } Backend
  6. interface InvestingStateManager { @Composable fun investingStates(): InvestingState } class FakeInvestingStateManager(

    defaultModel: InvestingState = DEFAULT_STATE, ) : InvestingStateManager { var investingStates by mutableStateOf(defaultModel) @Composable override fun investingStates(): InvestingState { return investingStates } } Backend
  7. ViewEventɾViewModel sealed class DependentWelcomeViewEvent { data object ButtonClicked : DependentWelcomeViewEvent()

    data class Close(val someData: String) : DependentWelcomeViewEvent() } data class DependentWelcomeViewModel( val toolbarTitle: String, val title: String, val buttonLabel: String, )
  8. ViewEventɾProtip sealed class LeftViewEvent { data object Left : LeftViewEvent()

    } sealed class RightViewEvent { data object Right : RightViewEvent() } sealed class CommonViewEvent { data object Yes : CommonViewEvent() data object No : CommonViewEvent() }
  9. ViewEventɾProtip sealed class LeftViewEvent { data object Left : LeftViewEvent()

    data class CommonEvent( val event: CommonViewEvent, ) : LeftViewEvent() } sealed class RightViewEvent { data object Right : RightViewEvent() data class CommonEvent( val event: CommonViewEvent, ) : RightViewEvent() } sealed class CommonViewEvent { data object Yes : CommonViewEvent() data object No : CommonViewEvent() }
  10. ViewEventɾProtip sealed class LeftViewEvent { data object Left : LeftViewEvent()

    data class CommonEvent( val event: CommonViewEvent, ) : LeftViewEvent() } sealed class RightViewEvent { data object Right : RightViewEvent() data class CommonEvent( val event: CommonViewEvent, ) : RightViewEvent() } sealed class CommonViewEvent { data object Yes : CommonViewEvent() data object No : CommonViewEvent() } fun handleEvent(event: LeftViewEvent) { when (event) { is Left -> TODO() is CommonEvent -> when (event.event) { is Yes -> TODO() is No -> TODO() } } }
  11. ViewEventɾProtip sealed interface LeftViewEvent { data object Left : LeftViewEvent

    data class CommonEvent( val event: CommonViewEvent, ) : LeftViewEvent } sealed interface RightViewEvent { data object Right : RightViewEvent data class CommonEvent( val event: CommonViewEvent, ) : RightViewEvent } sealed interface CommonViewEvent { data object Yes : CommonViewEvent data object No : CommonViewEvent } fun handleEvent(event: LeftViewEvent) { when (event) { is Left -> TODO() is CommonEvent -> when (event.event) { is Yes -> TODO() is No -> TODO() } } }
  12. ViewEventɾProtip sealed interface LeftViewEvent { data object Left : LeftViewEvent

    } sealed interface RightViewEvent { data object Right : RightViewEvent } sealed interface CommonViewEvent : LeftViewEvent, RightViewEvent { data object Yes : CommonViewEvent data object No : CommonViewEvent } fun handleEvent(event: LeftViewEvent) { when (event) { is Left -> TODO() is CommonEvent -> when (event.event) { is Yes -> TODO() is No -> TODO() } } }
  13. ViewEventɾProtip sealed interface LeftViewEvent { data object Left : LeftViewEvent

    } sealed interface RightViewEvent { data object Right : RightViewEvent } sealed interface CommonViewEvent : LeftViewEvent, RightViewEvent { data object Yes : CommonViewEvent data object No : CommonViewEvent } fun handleEvent(event: LeftViewEvent) { when (event) { is Left -> TODO() is Yes -> TODO() is No -> TODO() } }
  14. ViewEventɾProtip sealed interface LeftViewEvent { data object Left : LeftViewEvent

    } sealed interface RightViewEvent { data object Right : RightViewEvent } sealed interface CommonViewEvent : LeftViewEvent, RightViewEvent { data object Yes : CommonViewEvent data object No : CommonViewEvent } fun handleEvent(event: LeftViewEvent) { when (event) { is Left -> TODO() is Yes -> TODO() is No -> TODO() } } fun handleEvent(event: RightViewEvent) { when (event) { is Right -> TODO() is Yes -> TODO() is No -> TODO() } }
  15. interface Presenter<UiModel : Any, UiEvent : Any> { val models:

    Flow<UiModel> fun sendEvent(event: UiEvent) } Presenter
  16. interface Presenter<UiModel : Any, UiEvent : Any> { fun start(

    scope: CoroutineScope, ): Binding<UiModel, UiEvent> interface Binding<UiModel : Any, UiEvent : Any> { val models: Flow<UiModel> fun sendEvent(event: UiEvent) } } Presenter
  17. interface Presenter<UiModel : Any, UiEvent : Any> { fun start(

    scope: CoroutineScope, ): Binding<UiModel, UiEvent> interface Binding<UiModel : Any, UiEvent : Any> { // TODO: Change to StateFlow once all presenters can provide an initial value. val models: Flow<UiModel> fun sendEvent(event: UiEvent) } } Presenter
  18. @Deprecated("Use MoleculePresenter instead.") fun interface RxPresenter<UiEvent : Any, UiModel :

    Any> : ObservableTransformer<UiEvent, UiModel> @Deprecated("Use MoleculePresenter instead.") interface CoroutinePresenter<UiEvent : Any, UiModel : Any> { } interface MoleculePresenter<UiModel : Any, UiEvent : Any> { @Composable fun models( events: Flow<UiEvent>, ): UiModel } Presenter
  19. @Deprecated("Use MoleculePresenter instead.") fun interface RxPresenter<UiEvent : Any, UiModel :

    Any> : ObservableTransformer<UiEvent, UiModel> @Deprecated("Use MoleculePresenter instead.") interface CoroutinePresenter<UiEvent : Any, UiModel : Any> { } interface MoleculePresenter<UiModel : Any, UiEvent : Any> { @Composable fun models( events: Flow<UiEvent>, ): UiModel } Presenter
  20. @Deprecated("Use MoleculePresenter instead.") fun interface RxPresenter<UiEvent : Any, UiModel :

    Any> : ObservableTransformer<UiEvent, UiModel> @Deprecated("Use MoleculePresenter instead.") interface CoroutinePresenter<UiEvent : Any, UiModel : Any> { } interface MoleculePresenter<UiModel : Any, UiEvent : Any> { @Composable fun models( events: Flow<UiEvent>, ): UiModel } Presenter
  21. interface MoleculePresenter<UiModel : Any, UiEvent : Any> { @Composable fun

    models( events: Flow<UiEvent>, ): UiModel } Presenter
  22. class DependentWelcomePresenter( database: CashDatabase, private val stringManager: StringManager, @Io private

    val ioDispatcher: CoroutineContext, ) : MoleculePresenter<DependentWelcomeViewModel, DependentWelcomeViewEvent> { @Composable override fun models(events: Flow<DependentWelcomeViewEvent>): DependentWelcomeViewModel { LaunchedE f fect(events) { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { // Navigate back. } } }} val toolbarTitle by remember { database.stateQueries.select().asFlow().mapToOne(ioDispatcher).map { it.toolbar_title } }.collectAsState(stringManager[R.string.investing_tab_title]) return DependentWelcomeViewModel( toolbarTitle = toolbarTitle, title = stringManager[R.string.dependent_welcome_title], buttonLabel = stringManager[R.string.dependent_welcome_button_label], ) } } Presenter
  23. /** * Create a [Flow] which will continually recompose `body`

    to produce * a stream of [T] values when collected. */ fun <T> moleculeFlow( clock: RecompositionClock, body: @Composable () -> T, ): Flow<T> { // Stuff. } enum class RecompositionClock { ContextClock, Immediate, } Molecule
  24. /** * Launch a coroutine into this [CoroutineScope] which will

    * continually recompose `body` to produce a [StateFlow] stream * of [T] values. */ fun <T> CoroutineScope.launchMolecule( clock: RecompositionClock, body: @Composable () -> T, ): StateFlow<T> { // Stuff. } enum class RecompositionClock { ContextClock, Immediate, } Molecule
  25. val flow: Flow<ProfileModel> = moleculeFlow(Immediate) { profilePresenter(userFlow, balanceFlow) } val

    stateFlow: StateFlow<ProfileModel> = scope.launchMolecule(Immediate) { profilePresenter(userFlow, balanceFlow) } Molecule
  26. interface Ui<UiModel : Any, UiEvent : Any> { fun interface

    EventReceiver<UiEvent> { fun sendEvent(event: UiEvent) } fun setEventReceiver(receiver: EventReceiver<UiEvent>) } View
  27. interface Ui<UiModel : Any, UiEvent : Any> { fun interface

    EventReceiver<UiEvent> { fun sendEvent(event: UiEvent) } fun setEventReceiver(receiver: EventReceiver<UiEvent>) fun setModel(model: UiModel) } View
  28. class DependentView(context: Context) : View(context), Ui<DependentViewModel, DependentViewEvent> { override fun

    setEventReceiver(receiver: EventReceiver<DependentViewEvent>) { button.setOnClickListener { receiver.sendEvent(ButtonClicked) } toolbar.setNavigationOnClickListener { receiver.sendEvent(Close) } } override fun setModel(model: DependentWelcomeViewModel) { toolbar.title = model.toolbarTitle titleView.text = model.title button.text = model.buttonLabel } } View
  29. import androidx.compose.ui.platform.AbstractComposeView abstract class ComposeUiView<UiModel : Any, UiEvent : Any>(

    context: Context, attrs: AttributeSet? = null, ) : AbstractComposeView(context, attrs), Ui<UiModel, UiEvent> { @Composable abstract fun Content( model: UiModel?, onEvent: (UiEvent) -> Unit, ) } View
  30. class SettingView( context: Context, ) : ComposeUiView<SettingViewModel, SettingViewEvent>(context) { @Composable

    override fun Content( model: SettingViewModel?, onEvent: (SettingViewEvent) -> Unit, ) { // We can require it because our presenter emits synchronously. requireNotNull(model) SettingViewContent(model, onEvent) } } View
  31. @get:Rule val temporaryDatabase = TemporaryDatabase() @Test fun `discovery starts with

    no prices`() = runTest { investmentEntities().discoveryStocks(forSearch = false).test { assertThat(awaitItem()).isEqualTo(expected) entityPriceRefresher.currentPrices.onNext(amazonPrice) assertThat(awaitItem()).isEqualTo(otherExpected) ensureAllEventsConsumed() } } Testing
  32. open class TurbinesRule( val turbines: Turbines = Turbines(), ) :

    ExternalResource() { override fun after() { turbines.assertEmpty() } } Testing
  33. class FakeNavigator internal constructor() : Navigator { private val channels

    = Turbines() private val navigatedScreens = channels.create<Screen>() override fun goTo(screen: Screen) { navigatedScreens.add(screen) } suspend fun awaitNextScreen() = navigatedScreens.awaitItem() } Testing
  34. class FakeNavigator internal constructor() : Navigator { class Rule( val

    navigator: FakeNavigator = FakeNavigator(), ) : TurbinesRule(navigator.channels) private val channels = Turbines() private val navigatedScreens = channels.create<Screen>() override fun goTo(screen: Screen) { navigatedScreens.add(screen) } suspend fun awaitNextScreen() = navigatedScreens.awaitItem() } Testing
  35. class DependentWelcomePresenterTest { @get:Rule val navigatorRule = FakeNavigatorRule() private val

    navigator = navigatorRule.navigator private val stringManager = FakeStringManager( R.string.title to "Welcome", R.string.button_label to "Next", ) private val presenter = DependentWelcomePresenter( stringManager = stringManager, navigator = navigator, ) @Test fun models(): Unit = runTest { presenter.test { assertThat(awaitItem()).isEqualTo(expected)) } Testing
  36. ) private val presenter = DependentWelcomePresenter( stringManager = stringManager, navigator

    = navigator, ) @Test fun models(): Unit = runTest { presenter.test { assertThat(awaitItem()).isEqualTo(expected)) } } @Test fun `button clicks navigates`() = runTest { presenter.test { skipItem("Model") sendEvent(ButtonClicked) assertThat(navigator.awaitNextScreen()).isEqualTo(expected) } }
  37. class RealInvestingStateManagerTest { @get:Rule val temporaryDatabase = TemporaryDatabase() private val

    manager = RealInvestingStateManager( database = temporaryDatabase.cashDatabase, ioContext = EmptyCoroutineContext, ) @Test fun defaultState() = runTest { manager.test { assertThat(awaitItem()).isEqualTo(Loading) // Data has been fetched from the DB. assertThat(awaitItem()).isEqualTo(expected) } } companion object { private suspend fun InvestingStateManager.test(
  38. private val manager = RealInvestingStateManager( database = temporaryDatabase.cashDatabase, ioContext =

    EmptyCoroutineContext, ) @Test fun defaultState() = runTest { manager.test { assertThat(awaitItem()).isEqualTo(Loading) // Data has been fetched from the DB. assertThat(awaitItem()).isEqualTo(expected) } } companion object { private suspend fun InvestingStateManager.test( validate: suspend ReceiveTurbine<InvestingState>.() -> Unit, ) { moleculeFlow(Immediate) { investingStates() } .test { validate() } } } }
  39. class DependentWelcomeViewTest() { @get:Rule val paparazzi = Paparazzi( theme =

    “Theme.Cash.Default", ) @Test fun tests() { val view = DependentWelcomeView(context = paparazzi.context) view.setEventReceiver {} view.setModel( DependentWelcomeViewModel( toolbarTitle = "Stocks", title = "Stocks are now available for anyone", buttonLabel = "Next", ), ) paparazzi.snapshot(view) } } Ui Testing
  40. enum class AccessibilityTextSize(val scale: Float) { NORMAL(scale = 1f), LARGE(scale

    = 2f), } enum class DesignTheme(val theme: Theme, val colors: Colors) { Light(MooncakeLight, Colors.light), Dark(MooncakeDark, Colors.dark), ; val provide: Context.() -> ThemeInfo = {..} } class DependentWelcomeViewTest() { @get:Rule val paparazzi = Paparazzi( theme = “Theme.Cash.Default", ) Ui Testing
  41. Dark(MooncakeDark, Colors.dark), ; val provide: Context.() -> ThemeInfo } }

    @RunWith(TestParameterInjector::class) class DependentWelcomeViewTest( @TestParameter private val design: DesignTheme, @TestParameter private val accessibilityTextSize: AccessibilityTextSize, ) { @get:Rule val paparazzi = Paparazzi( theme = "Theme.Cash.Default", ) @Test fun tests() { val view = DependentWelcomeView(context = paparazzi.context).apply { setBackgroundColor(themeInfo().colorPalette.background) } view.setEventReceiver {} view.setModel( DependentWelcomeViewModel( toolbarTitle = "Stocks", title = "Stocks are now available for anyone", buttonLabel = "Next",
  42. val provide: Context.() -> ThemeInfo } } @RunWith(TestParameterInjector::class) class DependentWelcomeViewTest(

    @TestParameter private val design: DesignTheme, @TestParameter private val accessibilityTextSize: AccessibilityTextSize, ) { @get:Rule val paparazzi = Paparazzi( theme = "Theme.Cash.Default", deviceConfig = DeviceConfig.PIXEL_4 .copy(fontScale = accessibilityTextSize.scale), ) private val context: Context by lazy { val themeInfo = design.provide(paparazzi.context) paparazzi.context.wrapWithTheme { themeInfo } } @Test fun tests() { val view = DependentWelcomeView(context = context).apply { setBackgroundColor(themeInfo().colorPalette.background) } view.setEventReceiver {} view.setModel(
  43. - Write tests. - Record snapshots: ./gradlew module:recordPaparazziDebug - Check

    snapshots into the repository. - Run verify task on CI: ./gradlew module:verifyPaparazziDebug Ui Testing
  44. interface Navigator { fun goTo(screen: Screen) } LaunchedE f fect(events)

    { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { // Navigate. } } } } Navigation
  45. interface Navigator { fun goTo(screen: Screen) } LaunchedE f fect(events)

    { events.collect { event -> when (event) { ButtonClicked -> { // Start flow. } Close -> { navigator.goTo(SomeScreen()) } } } } Navigation
  46. interface ViewFactory { fun createView( screen: Screen, parent: ViewGroup, ):

    Ui<*, *>? } interface PresenterFactory { fun create( screen: Screen, navigator: Navigator, ): Presenter<*, *>? } interface TransitionFactory { fun createTransition( fromScreen: Screen, fromView: View, toScreen: Screen, toView: View, parent: ViewGroup, ): Animator? = null }
  47. fun MoleculePresenter<UiEvent, UiModel>.asPresenter() : Presenter<UiModel, UiEvent> { return object :

    Presenter<UiModel, UiEvent> { override fun start( scope: CoroutineScope, ): Presenter.Binding<UiModel, UiEvent> { // Stuff. } } }
  48. class InvestingViewFactory @Inject internal constructor( private val customOrder: InvestingCustomOrderView.Factory, private

    val featureFlag: FeatureFlagManager, ) : BroadwayViewFactory { override fun createView(screen: Screen, context: Context, parent: ViewGroup): View? { val view = when (screen) { is DependentWelcomeScreen -> DependentWelcomeView(context) is CustomOrderScreen -> customOrder.build(context) is NotificationSettings -> if (featureFlag.something()) { InvestingNotificationSettingsNew(context) } else { InvestingNotificationSettingsLegacy(context) } else -> XmlFactory.inflate( context = context, layoutResId = when (screen) { is InvestingHome -> R.layout.investing_home else -> return null }, parent = parent, ) } return view
  49. class InvestingPresenterFactory @Inject internal constructor( private val notificationSettingsPresenter: InvestingNotificationSettingsPresenter.Factory, private

    val customOrderPresenter: InvestingCustomOrderPresenter.Factory, private val investingHomePresenter: InvestingHomePresenter.Factory, private val dependentWelcomePresenter: DependentWelcomePresenter.Factory, ) : PresenterFactory { override fun create( screen: Screen, navigator: Navigator, ): Presenter<*, *>? { return when (screen) { is DependentWelcomeScreen -> dependentWelcomePresenter.construct(navigator).asPresenter( is CustomOrderScreen -> customOrderPresenter.create(screen, navigator).asPresenter() is InvestingHome -> investingHomePresenter.create(navigator, screen).asPresenter() is NotificationSettings -> notificationSettingsPresenter.create(screen, navigator).asPre else -> null } } }
  50. class Broadway( private val viewFactories: List<ViewFactory> = listOf(), private val

    transitionFactories: List<TransitionFactory> = listOf(), private val presenterFactories: List<PresenterFactory> = listOf(), ) { fun createViewOrNull(...): ScreenView? { return viewFactories.asSequence().mapNotNull { it.createView(screen, themedContext, parent) }.firstOrNull() } fun createTransition(...): Animator? { return transitionFactories.asSequence().mapNotNull { it.createTransition(fromScreen, fromView, toScreen, toView, parent) }.firstOrNull() } fun createPresenter(screen: Screen, navigator: Navigator): Presenter<*, *>? { return presenterFactories.asSequence().mapNotNull { it.create(screen, navigator) }.firstOrNull() } }
  51. class RealNavigator( private val broadway: Broadway, ) : FrameLayout(context), Navigator

    { fun goTo(screen: Screen) { val presenter = broadway.createPresenter(screen, navigator = this) val ui = broadway.createUi(screen, context) bindAndAttachAndSwapAndStuff(ui, presenter) } }
  52. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter
  53. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel()
  54. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state
  55. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter
  56. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull()
  57. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope)
  58. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI
  59. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull()
  60. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate
  61. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter
  62. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) }
  63. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) } - Deliver results
  64. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) } - Deliver results - Attach UI and runs transition
  65. - navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

    - Back stack management - Tear down old presenter - currentPresenterScope.cancel() - Save UI state - Spin up new presenter - val newPresenter = presenterFactories.asSequence() .mapNotNull { it.create(screen, navigator) } .firstOrNull() - newPresenter.start(scope) - Create UI - val newUi = uiFactories.asSequence() .mapNotNull { it.createUi(screen, themedContext, parent) } .firstOrNull() - Restore UI state if adequate - Bind UI to Presenter - newPresenterScope.launch { ui.setEventReceiver(presenter::sendEvent) presenter.models.collect(ui::setModel) } - Deliver results - Attach UI and runs transition - Remove old UI
  66. App

  67. App

  68. Workers /** Arbitrary code that needs to run as the

    result of [MainActivity]'s creation. */ interface ActivityWorker { /** * Called during `MainActivity.onCreate`. Implementers may assume that [work] invocations are * never cancelled. */ suspend fun work(lifecycle: Lifecycle) } /** Arbitrary code that needs to run as the result of application creation. */ interface ApplicationWorker { /** * Called once during `Application.onCreate` to initialise this worker. To receive other * `Application` lifecycle events inject an `Observable<ApplicationEvent>` and react to its * emissions. * * Implementers may assume that [work] invocations will never be cancelled. */ suspend fun work() }
  69. Development process • No feature branch. • Feature flag! •

    Small and stacked PRs. • PR Reviews? Trust by default. • Avoid bike-shedding with automation and lint. • Version bump are mostly automatic.
  70. Android Test? • Just a very few. • Mock mode:

    no network dependency. • Runtime check.
  71. Design System • Own Repo • Artifact for all platforms

    • Colors, Icons, Dimensions, Typography • Website to browse it! • Figma uses it • Tools to migrate
  72. Text( modifier = Modifier.padding(vertical = 6.dp), text = "Good stuff",

    style = SecretTheme.typography.body, color = SecretTheme.colors.semantic.text.prominent, ) Design System
  73. Functional Teams • Independent team / feature • PM/Design/Server/Mobile •

    Bottom-up! but… • Pros • Velocity • Cons • Conflict
  74. Open Source • All part of our tech stack. •

    Any can do it. • Part of the work.
  75. Fin

  76. References • Cash App Code Blog • https://code.cash.app/ • Square

    on Github • https://github.com/square • Cash App on Github • https://github.com/cashapp • Variants on steroids with Dagger • https://github.com/JakeWharton/u2020/ • Code that last forever • https://www.youtube.com/watch?v=YZstpc2939s • Debug builds • https://www.youtube.com/watch?v=Ae4vqz29W9U