Slide 1

Slide 1 text

36ŋ15

Slide 2

Slide 2 text

Cash App

Slide 3

Slide 3 text

Cash App

Slide 4

Slide 4 text

Cash App

Slide 5

Slide 5 text

Cash App • 60~ developpers • 1_350~ modules • 160_000~ lines of Kotlin

Slide 6

Slide 6 text

Protobuf Backend DB Network Presenter View Screen

Slide 7

Slide 7 text

Protobuf

Slide 8

Slide 8 text

• API contract • Code generation • Efficient Protobuf

Slide 9

Slide 9 text

Protobuf

Slide 10

Slide 10 text

Protobuf

Slide 11

Slide 11 text

Network

Slide 12

Slide 12 text

Network

Slide 13

Slide 13 text

Network

Slide 14

Slide 14 text

Network

Slide 15

Slide 15 text

Database

Slide 16

Slide 16 text

Database

Slide 17

Slide 17 text

Database

Slide 18

Slide 18 text

• Service • Manager • Syncer • Repository Backend

Slide 19

Slide 19 text

interface InvestingAppService { @POST("/2.0/cash/investing/get-discovery") suspend fun getDiscovery( @Body request: GetDiscoveryRequest, ): ApiResult @POST("/2.0/cash/investing/get-customer-settings") fun getCustomerSettings( @Body request: GetCustomerSettingsRequest, ): Single> } Backend

Slide 20

Slide 20 text

sealed class ApiResult { data class Success(val response: T) : ApiResult() sealed class Failure : ApiResult() { data class NetworkFailure(val error: Throwable) : Failure() data class HttpFailure( val code: Int, val responseHeaderDate: Date? = null, ) : Failure() } } Backend

Slide 21

Slide 21 text

interface InvestmentEntities { fun discoveryStocks(forSearch: Boolean): Flow fun stockDetails(token: InvestmentEntityToken): Flow } Backend

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

interface InvestmentEntities { fun discoveryStocks(forSearch: Boolean): Flow fun stockDetails(token: InvestmentEntityToken): Flow } class FakeInvestmentEntities : InvestmentEntities { val discoveryAll = MutableSharedFlow(replay = 1) val discoveryForSearch = MutableSharedFlow(replay = 1) override fun discoveryStocks( forSearch: Boolean, ): Flow { return if (forSearch) discoveryForSearch else discoveryAll } Backend

Slide 24

Slide 24 text

interface InvestingStateManager { @Composable fun investingStates(): InvestingState } Backend

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

MVW Architecture

Slide 28

Slide 28 text

Model-View-Whatever

Slide 29

Slide 29 text

Model-View-Whatever • Unidirectional Data Flow • Single source of truth • Immutability • Side-effect isolation

Slide 30

Slide 30 text

Protobuf Backend DB Network Presenter View MVW

Slide 31

Slide 31 text

Presenter View MVW

Slide 32

Slide 32 text

Presenter View ViewEvent ViewModel MVW

Slide 33

Slide 33 text

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, )

Slide 34

Slide 34 text

ViewEventɾProtip

Slide 35

Slide 35 text

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() }

Slide 36

Slide 36 text

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() }

Slide 37

Slide 37 text

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() } } }

Slide 38

Slide 38 text

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() } } }

Slide 39

Slide 39 text

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() } } }

Slide 40

Slide 40 text

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() } }

Slide 41

Slide 41 text

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() } }

Slide 42

Slide 42 text

interface Presenter { } Presenter

Slide 43

Slide 43 text

interface Presenter { val models: Flow fun sendEvent(event: UiEvent) } Presenter

Slide 44

Slide 44 text

interface Presenter { fun start( scope: CoroutineScope, ): Binding interface Binding { val models: Flow fun sendEvent(event: UiEvent) } } Presenter

Slide 45

Slide 45 text

interface Presenter { fun start( scope: CoroutineScope, ): Binding interface Binding { // TODO: Change to StateFlow once all presenters can provide an initial value. val models: Flow fun sendEvent(event: UiEvent) } } Presenter

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

interface MoleculePresenter { @Composable fun models( events: Flow, ): UiModel } Presenter

Slide 50

Slide 50 text

class DependentWelcomePresenter( database: CashDatabase, private val stringManager: StringManager, @Io private val ioDispatcher: CoroutineContext, ) : MoleculePresenter { @Composable override fun models(events: Flow): 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

Slide 51

Slide 51 text

Molecule

Slide 52

Slide 52 text

/** * Create a [Flow] which will continually recompose `body` to produce * a stream of [T] values when collected. */ fun moleculeFlow( clock: RecompositionClock, body: @Composable () -> T, ): Flow { // Stuff. } enum class RecompositionClock { ContextClock, Immediate, } Molecule

Slide 53

Slide 53 text

/** * Launch a coroutine into this [CoroutineScope] which will * continually recompose `body` to produce a [StateFlow] stream * of [T] values. */ fun CoroutineScope.launchMolecule( clock: RecompositionClock, body: @Composable () -> T, ): StateFlow { // Stuff. } enum class RecompositionClock { ContextClock, Immediate, } Molecule

Slide 54

Slide 54 text

val flow: Flow = moleculeFlow(Immediate) { profilePresenter(userFlow, balanceFlow) } val stateFlow: StateFlow = scope.launchMolecule(Immediate) { profilePresenter(userFlow, balanceFlow) } Molecule

Slide 55

Slide 55 text

interface Ui { } View

Slide 56

Slide 56 text

interface Ui { fun interface EventReceiver { fun sendEvent(event: UiEvent) } fun setEventReceiver(receiver: EventReceiver) } View

Slide 57

Slide 57 text

interface Ui { fun interface EventReceiver { fun sendEvent(event: UiEvent) } fun setEventReceiver(receiver: EventReceiver) fun setModel(model: UiModel) } View

Slide 58

Slide 58 text

class DependentView(context: Context) : View(context), Ui { override fun setEventReceiver(receiver: EventReceiver) { 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

Slide 59

Slide 59 text

import androidx.compose.ui.platform.AbstractComposeView abstract class ComposeUiView( context: Context, attrs: AttributeSet? = null, ) : AbstractComposeView(context, attrs), Ui { @Composable abstract fun Content( model: UiModel?, onEvent: (UiEvent) -> Unit, ) } View

Slide 60

Slide 60 text

class SettingView( context: Context, ) : ComposeUiView(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

Slide 61

Slide 61 text

Protobuf Backend DB Network Presenter View Testing

Slide 62

Slide 62 text

Protobuf Backend DB Network Presenter View Testing

Slide 63

Slide 63 text

Testing

Slide 64

Slide 64 text

@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

Slide 65

Slide 65 text

open class TurbinesRule( val turbines: Turbines = Turbines(), ) : ExternalResource() { override fun after() { turbines.assertEmpty() } } Testing

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

) 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) } }

Slide 70

Slide 70 text

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(

Slide 71

Slide 71 text

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.() -> Unit, ) { moleculeFlow(Immediate) { investingStates() } .test { validate() } } } }

Slide 72

Slide 72 text

Ui Testing

Slide 73

Slide 73 text

Ui Testing

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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",

Slide 77

Slide 77 text

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(

Slide 78

Slide 78 text

Ui Testing

Slide 79

Slide 79 text

Ui Testing

Slide 80

Slide 80 text

- Write tests. Ui Testing

Slide 81

Slide 81 text

- Write tests. - Record snapshots: ./gradlew module:recordPaparazziDebug Ui Testing

Slide 82

Slide 82 text

- Write tests. - Record snapshots: ./gradlew module:recordPaparazziDebug - Check snapshots into the repository. Ui Testing

Slide 83

Slide 83 text

- Write tests. - Record snapshots: ./gradlew module:recordPaparazziDebug - Check snapshots into the repository. - Run verify task on CI: ./gradlew module:verifyPaparazziDebug Ui Testing

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

No content

Slide 88

Slide 88 text

No content

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

Navigation

Slide 92

Slide 92 text

interface Screen : Parcelable Navigation

Slide 93

Slide 93 text

interface Screen : Parcelable @Parcelize data class StockDetails( val entityToken: EntityToken, ) : Screen Navigation

Slide 94

Slide 94 text

interface Navigator { fun goTo(screen: Screen) } Navigation

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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 }

Slide 98

Slide 98 text

fun MoleculePresenter.asPresenter() : Presenter { return object : Presenter { override fun start( scope: CoroutineScope, ): Presenter.Binding { // Stuff. } } }

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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 } } }

Slide 101

Slide 101 text

class Broadway( private val viewFactories: List = listOf(), private val transitionFactories: List = listOf(), private val presenterFactories: List = 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() } }

Slide 102

Slide 102 text

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) } }

Slide 103

Slide 103 text

- navigator.goTo(SomeScreen)

Slide 104

Slide 104 text

- navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta.

Slide 105

Slide 105 text

- navigator.goTo(SomeScreen) - Determine the screen type: full, overlay, meta. - Back stack management

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

- 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

Slide 110

Slide 110 text

- 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()

Slide 111

Slide 111 text

- 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)

Slide 112

Slide 112 text

- 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

Slide 113

Slide 113 text

- 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()

Slide 114

Slide 114 text

- 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

Slide 115

Slide 115 text

- 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

Slide 116

Slide 116 text

- 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) }

Slide 117

Slide 117 text

- 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

Slide 118

Slide 118 text

- 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

Slide 119

Slide 119 text

- 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

Slide 120

Slide 120 text

App

Slide 121

Slide 121 text

ɹɹɹType ɹɹFlavorɹ Debug Release Internal internalDebug internalRelease Production productionDebug productionRelease App

Slide 122

Slide 122 text

ɹɹɹType ɹɹFlavorɹ Debug Release Internal Dev Alpha Production productionDebug Prod App

Slide 123

Slide 123 text

Process restart

Slide 124

Slide 124 text

Bug reporting

Slide 125

Slide 125 text

Variant implementation

Slide 126

Slide 126 text

Variant implementation

Slide 127

Slide 127 text

App

Slide 128

Slide 128 text

Content Container/Navigator Activity

Slide 129

Slide 129 text

Debug Drawer Content Debug View Container/Navigator Activity

Slide 130

Slide 130 text

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` and react to its * emissions. * * Implementers may assume that [work] invocations will never be cancelled. */ suspend fun work() }

Slide 131

Slide 131 text

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.

Slide 132

Slide 132 text

No content

Slide 133

Slide 133 text

No content

Slide 134

Slide 134 text

Dependency diff

Slide 135

Slide 135 text

Bundles diff

Slide 136

Slide 136 text

No content

Slide 137

Slide 137 text

No content

Slide 138

Slide 138 text

Android Test? • Just a very few. • Mock mode: no network dependency. • Runtime check.

Slide 139

Slide 139 text

Mobile Releases

Slide 140

Slide 140 text

Design System • Own Repo • Artifact for all platforms • Colors, Icons, Dimensions, Typography • Website to browse it! • Figma uses it • Tools to migrate

Slide 141

Slide 141 text

No content

Slide 142

Slide 142 text

Design System

Slide 143

Slide 143 text

Design System

Slide 144

Slide 144 text

No content

Slide 145

Slide 145 text

No content

Slide 146

Slide 146 text

Text( modifier = Modifier.padding(vertical = 6.dp), text = "Good stuff", style = SecretTheme.typography.body, color = SecretTheme.colors.semantic.text.prominent, ) Design System

Slide 147

Slide 147 text

Functional Teams • Independent team / feature • PM/Design/Server/Mobile • Bottom-up! but… • Pros • Velocity • Cons • Conflict

Slide 148

Slide 148 text

Dynamic Feature

Slide 149

Slide 149 text

The Future

Slide 150

Slide 150 text

The Future Now

Slide 151

Slide 151 text

Open Source • All part of our tech stack. • Any can do it. • Part of the work.

Slide 152

Slide 152 text

Fin

Slide 153

Slide 153 text

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