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

Sweet Architecture

Sweet Architecture

How about unidirectional data flow in an architecture where views and presenters don’t know about each other? While growing up to 40+ engineers and 600+ modules, Cash App managed to control the complexity of its product. Views can be written in either Kotlin or XML; presenters with either Rx, Coroutines, or Compose; no problem. We test views on the JVM and we don’t need to define fake presenters either.

Writing new screens is delightful and we’ll see how we made it possible by:
- Looking at the foundations of the architecture: our internal navigation library which allows clear modularity,
- Checking how it can adapt to presenters using different technologies,
- Explaining how views are defined and tested,
- Seeing how everything is glued together from a bird’s-eye view of the app.

Growing your app and team doesn’t imply more pain nor more complexity. Attendees will gain a sound understanding about how we achieved it.

Benoît Quenaudon

April 28, 2023
Tweet

More Decks by Benoît Quenaudon

Other Decks in Programming

Transcript

  1. weet
    Architecture

    View Slide

  2. Activities, fragments, view models, navigation, layouts…
    Android APIs

    View Slide

  3. Dianne Hackborn: « It is probably better to call the core Android
    APIs a "system framework." For the most part, the platform APIs
    we provide are there to de
    fi
    ne how an application interacts with
    the operating system; but for anything going on purely within
    the app, these APIs are often just not relevant. »
    Android APIs

    View Slide

  4. Ui

    View Slide

  5. class CardView(


    private val screen: CardViewScreen,


    private val presenterFactory: CardPresenter.Factory,


    context: Context,


    ) : ContourLayout(context) {


    private val events = PublishRelay.create()


    override fun onAttachedToWindow() {


    super.onAttachedToWindow()


    }


    }

    View Slide

  6. class CardView(


    private val screen: CardViewScreen,


    private val presenterFactory: CardPresenter.Factory,


    context: Context,


    ) : ContourLayout(context) {


    private val events = PublishRelay.create()


    override fun onAttachedToWindow() {


    super.onAttachedToWindow()


    events


    .compose(presenterFactory.create(screen))


    .subscribe(this::setModel)


    }


    }

    View Slide

  7. class CardView(


    private val screen: CardViewScreen,


    private val presenterFactory: CardPresenter.Factory,


    context: Context,


    ) : ContourLayout(context) {


    private val events = PublishRelay.create()


    override fun onAttachedToWindow() {


    super.onAttachedToWindow()


    events


    .compose(presenterFactory.create(screen))


    .takeUntil(detaches())


    .subscribe(this::setModel)


    }


    }
    NO-NO

    View Slide

  8. Presenter
    UiModel
    UiEvent
    Ui

    View Slide

  9. Presenter
    UiModel
    UiEvent
    Ui Ui

    View Slide

  10. View Slide

  11. data class DependentWelcomeViewModel(


    val toolbarTitle: String,


    val title: String,


    val subTitle: String,


    val ctaLabel: String,


    )


    sealed interface DependentWelcomeViewEvent {


    object Close : DependentWelcomeViewEvent


    object CtaClicked : DependentWelcomeViewEvent


    }

    View Slide

  12. Ui

    View Slide

  13. interface Ui {


    }

    View Slide

  14. interface Ui {


    interface EventReceiver {


    fun sendEvent(event: UiEvent)


    }


    fun setEventReceiver(receiver: EventReceiver)


    }

    View Slide

  15. interface Ui {


    interface EventReceiver {


    fun sendEvent(event: UiEvent)


    }


    fun setEventReceiver(receiver: EventReceiver)


    fun setModel(model: UiModel)


    }

    View Slide

  16. class DependentWelcomeView(


    context: Context,


    attrs: AttributeSet? = null,


    ) : View(context, attrs),


    Ui {


    }


    View Slide

  17. class DependentWelcomeView(


    context: Context,


    attrs: AttributeSet? = null,


    ) : View(context, attrs),


    Ui {


    // views and buttons setup…


    }


    View Slide

  18. class DependentWelcomeView(


    context: Context,


    attrs: AttributeSet? = null,


    ) : View(context, attrs),


    Ui {


    // views and buttons setup…


    override fun setEventReceiver(receiver: EventReceiver) {


    }
    override fun setModel(model: DependentWelcomeViewModel) {


    }


    }


    View Slide

  19. class DependentWelcomeView(


    context: Context,


    attrs: AttributeSet? = null,


    ) : View(context, attrs),


    Ui {


    // views and buttons setup…


    override fun setEventReceiver(receiver: EventReceiver) {


    button.setOnClickListener { receiver.sendEvent(CtaClicked) }


    toolbar.setNavigationOnClickListener { receiver.sendEvent(Close) }


    }
    override fun setModel(model: DependentWelcomeViewModel) {


    }


    }


    View Slide

  20. class DependentWelcomeView(


    context: Context,


    attrs: AttributeSet? = null,


    ) : View(context, attrs),


    Ui {


    // views and buttons setup…


    override fun setEventReceiver(receiver: EventReceiver) {


    button.setOnClickListener { receiver.sendEvent(CtaClicked) }


    toolbar.setNavigationOnClickListener { receiver.sendEvent(Close) }


    }
    override fun setModel(model: DependentWelcomeViewModel) {


    toolbar.title = model.toolbarTitle


    titleView.text = model.title


    subtitleView.text = model.subTitle


    button.text = model.ctaLabel


    }


    }


    View Slide

  21. abstract class ComposeUiView(


    context: Context,


    attrs: AttributeSet? = null,


    ) : AbstractComposeView(context, attrs), Ui {


    @Composable


    abstract fun Content(


    model: UiModel?,


    onEvent: (UiEvent) -> Unit,


    ) {


    // Deal with setModel/eventReceiver stuff.


    }


    }

    View Slide

  22. Ui tests

    View Slide

  23. View Slide

  24. Paparazzi

    View Slide

  25. class DependentWelcomeViewTest(


    ) {


    @get:Rule val paparazzi = Paparazzi(


    theme = "Theme.Cash.Default",


    )


    @Test fun tests() {


    }


    }
    Paparazzi: Legacy UI Test


    https://github.com/cashapp/paparazzi

    View Slide

  26. class DependentWelcomeViewTest(


    ) {


    @get:Rule val paparazzi = Paparazzi(


    theme = "Theme.Cash.Default",


    )


    @Test fun tests() {


    val view = DependentWelcomeView(context = context)


    view.setModel(


    DependentWelcomeViewModel(


    toolbarTitle = "Stocks",


    title = "Stocks are now available for anyone 13+",


    subTitle = "Request approval from your account sponsor to...”,


    ctaLabel = "Continue",


    )


    )


    paparazzi.snapshot(view)


    }


    }
    Paparazzi: Legacy UI Test


    https://github.com/cashapp/paparazzi

    View Slide

  27. @RunWith(TestParameterInjector::class)


    class DependentWelcomeViewTest(


    @TestParameter private val design: DesignTheme,


    ) {


    @get:Rule val paparazzi = Paparazzi(


    theme = "Theme.Cash.Default",


    )


    @Test fun tests() {


    val view = DependentWelcomeView(context = context)


    view.setModel(


    DependentWelcomeViewModel(


    toolbarTitle = "Stocks",


    title = "Stocks are now available for anyone 13+",


    subTitle = "Request approval from your account sponsor to...”,


    ctaLabel = "Continue",


    )


    )


    paparazzi.snapshot(view)


    }


    }
    Paparazzi: Legacy UI Test


    https://github.com/cashapp/paparazzi

    View Slide

  28. @RunWith(TestParameterInjector::class)


    class DependentWelcomeViewTest(


    @TestParameter private val design: DesignTheme,


    ) {


    @get:Rule val paparazzi = Paparazzi(


    theme = "Theme.Cash.Default",


    )


    private val context: Context by lazy {


    paparazzi.context


    .wrapWithTheme { design.provide(paparazzi.context) }


    }


    @Test fun tests() {


    val view = DependentWelcomeView(context = context)


    view.setModel(


    DependentWelcomeViewModel(


    toolbarTitle = "Stocks",


    title = "Stocks are now available for anyone 13+",


    subTitle = "Request approval from your account sponsor to...”,


    ctaLabel = "Continue",


    )


    )


    paparazzi.snapshot(view)


    }


    }
    Paparazzi: Legacy UI Test


    https://github.com/cashapp/paparazzi

    View Slide

  29. class HelloComposeTest {


    @get:Rule


    val paparazzi = Paparazzi()


    @Test


    fun compose() {


    paparazzi.snapshot(HelloPaparazzi())


    }


    }


    @Composable


    fun HelloPaparazzi() {


    Column(


    Modifier


    .background(Color.White)


    .fillMaxSize()


    .wrapContentSize()


    ) {


    // Stuff.


    }
    Paparazzi: Compose UI Test


    https://github.com/cashapp/paparazzi

    View Slide

  30. Paparazzi: Reports

    View Slide

  31. - Write tests.


    - Record snapshots: ./gradlew module:recordPaparazziDebug


    - Check snapshots into the repository.


    - Run verify task on CI: ./gradlew module:verifyPaparazziDebug
    Paparazzi: CI work
    fl
    ow


    https://github.com/cashapp/paparazzi

    View Slide

  32. Paparazzi: CI work
    fl
    ow


    https://github.com/cashapp/paparazzi

    View Slide

  33. Paparazzi: CI work
    fl
    ow


    https://github.com/cashapp/paparazzi

    View Slide

  34. Paparazzi: CI work
    fl
    ow


    https://github.com/cashapp/paparazzi

    View Slide

  35. runs on
    JVM
    Paparazzi


    https://github.com/cashapp/paparazzi

    View Slide

  36. Presenter

    View Slide

  37. interface Presenter {


    }

    View Slide

  38. interface Presenter {


    val models: Flow


    fun sendEvent(event: UiEvent)


    }

    View Slide

  39. interface Presenter {


    interface Binding {


    val models: Flow


    fun sendEvent(event: UiEvent)


    }


    }

    View Slide

  40. interface Presenter {


    fun start(scope: CoroutineScope): Binding


    interface Binding {


    val models: Flow


    fun sendEvent(event: UiEvent)


    }


    }

    View Slide

  41. interface Presenter {


    fun start(scope: CoroutineScope): Binding


    interface Binding {


    // TODO: Change to StateFlow.


    val models: Flow


    fun sendEvent(event: UiEvent)


    }


    }

    View Slide

  42. Presenter
    UiModel
    UiEvent
    Ui Ui

    View Slide

  43. fun map(event): model

    View Slide

  44. interface ObservableTransformer {


    fun apply(@NonNull Observable events): ObservableSource


    }


    View Slide

  45. interface ObservableTransformer {


    fun apply(@NonNull Observable events): ObservableSource


    }


    interface CoroutinePresenter {


    suspend fun produceModels(


    events: Flow,


    emit: FlowCollector,


    )


    }

    View Slide

  46. class DependentWelcomePresenterOT(


    private val stringManager: StringManager,


    private val database: CashDatabase,


    @Io private val ioScheduler: Scheduler,


    ) : ObservableTransformer {


    override fun apply(


    events: Observable


    ): ObservableSource {


    }


    }
    Observable Transformer

    View Slide

  47. class DependentWelcomePresenterOT(


    private val stringManager: StringManager,


    private val database: CashDatabase,


    @Io private val ioScheduler: Scheduler,


    ) : ObservableTransformer {


    override fun apply(


    events: Observable


    ): ObservableSource {


    return events.publish { events ->


    Observable.merge(


    events.filterIsInstance().startFlow(),


    events.filterIsInstance().goBack(),


    )


    }


    }


    private fun Observable.startFlow(): Observable {


    return consumeOnNext { // Start f
    l
    ow. }


    }


    private fun Observable.goBack(): Observable {


    return consumeOnNext { // Navigate back. }


    }


    }
    Observable Transformer

    View Slide



  48. ): ObservableSource {


    return events.publish { events ->


    Observable.merge(


    events.filterIsInstance().startFlow(),


    events.filterIsInstance().goBack(),


    models(),


    )


    }


    }


    private fun models(): Observable {


    return database.stateQueries.select().asObservable(ioScheduler).mapToOne()


    .map { it.toolbar_title }


    .startWith(stringManager[R.string.investing_tab_title])


    .map {


    DependentWelcomeViewModel(


    toolbarTitle = it,


    title = stringManager[R.string.dependent_welcome_title],


    subTitle = stringManager[R.string.dependent_welcome_subtitle],


    ctaLabel = stringManager[R.string.dependent_welcome_cta_label],


    )


    }


    }


    private fun Observable.startFlow(): Observable {


    return consumeOnNext { // Start f
    l
    ow. }


    }


    private fun Observable.goBack(): Observable {


    return consumeOnNext { // Navigate back. }




    Observable Transformer

    View Slide

  49. @Test fun models() {


    val events = PublishRelay.create()


    val models = events.compose(presenter).test(rxRule)


    models.assertValue(


    DependentWelcomeViewModel(


    toolbarTitle = stringManager[R.string.investing_tab_title],


    title = stringManager[R.string.dependent_welcome_title],


    subTitle = stringManager[R.string.dependent_welcome_subtitle],


    ctaLabel = stringManager[R.string.dependent_welcome_cta_label],


    )


    )


    }


    Observable Transformer

    View Slide

  50. @Test fun models() {


    val events = PublishRelay.create()


    val models = events.compose(presenter).test(rxRule)


    models.assertValue(


    DependentWelcomeViewModel(


    toolbarTitle = stringManager[R.string.investing_tab_title],


    title = stringManager[R.string.dependent_welcome_title],


    subTitle = stringManager[R.string.dependent_welcome_subtitle],


    ctaLabel = stringManager[R.string.dependent_welcome_cta_label],


    )


    )


    }


    @Test fun `cta clicks navigates starts invest teen request authorization flow`() {


    val events = PublishRelay.create()


    val models = events.compose(presenter).test(rxRule)


    models.assertAnyValue() // Default model emitted.


    events.accept(CtaClicked)


    assertThat(navigator.takeNextScreen()).isEqualTo(InvestingHome())


    }
    Observable Transformer

    View Slide

  51. Molecule

    View Slide

  52. @Composable


    fun Content(message: Message) {


    Column {


    Text(text = message.author)


    Text(text = message.body)


    }


    }
    Molecule


    https://github.com/cashapp/molecule

    View Slide

  53. @Composable


    fun profilePresenter(


    userFlow: Flow,


    balanceFlow: Flow,


    ): ProfileModel {


    val user: User? by userFlow.collectAsState(null)


    val balance: Long by balanceFlow.collectAsState(0L)


    return if (user == null) {


    ProfileModel.Loading


    } else {


    ProfileModel.Data(user.name, balance)


    }


    }
    Molecule


    https://github.com/cashapp/molecule

    View Slide

  54. /**


    * 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.


    }


    Molecule


    https://github.com/cashapp/molecule
    enum class RecompositionClock {


    ContextClock,


    Immediate,


    }

    View Slide

  55. /**


    * 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.


    }


    Molecule


    https://github.com/cashapp/molecule
    enum class RecompositionClock {


    ContextClock,


    Immediate,


    }

    View Slide

  56. val flow: Flow = moleculeFlow(Immediate) {


    profilePresenter(userFlow, balanceFlow)


    }


    val stateFlow: StateFlow = scope.launchMolecule(ContextClock) {


    profilePresenter(userFlow, balanceFlow)


    }
    Molecule


    https://github.com/cashapp/molecule

    View Slide

  57. interface ObservableTransformer {


    fun apply(@NonNull Observable upstream): ObservableSource


    }


    interface CoroutinePresenter {


    suspend fun produceModels(


    events: Flow,


    emit: FlowCollector,


    )


    }

    View Slide

  58. interface CoroutinePresenter {


    suspend fun produceModels(


    events: Flow,


    emit: FlowCollector,


    )


    }


    interface MoleculePresenter {


    @Composable


    fun models(


    events: Flow


    ): UiModel


    }

    View Slide

  59. class DependentWelcomePresenter(


    database: CashDatabase,


    private val stringManager: StringManager,


    @Io private val ioContext: CoroutineContext,


    ) : MoleculePresenter {


    @Composable


    override fun models(events: Flow): DependentWelcomeViewModel {


    }


    }
    Molecule Presenter

    View Slide

  60. class DependentWelcomePresenter(


    database: CashDatabase,


    private val stringManager: StringManager,


    @Io private val ioContext: CoroutineContext,


    ) : MoleculePresenter {


    @Composable


    override fun models(events: Flow): DependentWelcomeViewModel {


    LaunchedE
    f
    fect(events) {


    events.collect { event ->


    when (event) {


    CtaClicked -> { // Start flow. }


    Close -> { // Navigate back. }


    }


    }


    }


    }


    }
    Molecule Presenter

    View Slide



  61. private val stringManager: StringManager,


    @Io private val ioContext: CoroutineContext,


    ) : MoleculePresenter {


    @Composable


    override fun models(events: Flow): DependentWelcomeViewModel {


    LaunchedE
    f
    fect(events) {


    events.collect { event ->


    when (event) {


    CtaClicked -> { // Start flow. }


    Close -> { // Navigate back. }


    }


    }


    }




    val toolbarTitle by remember {


    database.stateQueries.select().asFlow().mapToOne(ioContext)


    .map { it.toolbar_title }


    }.collectAsState(stringManager[R.string.investing_tab_title])


    return DependentWelcomeViewModel(


    toolbarTitle = toolbarTitle,


    title = stringManager[R.string.dependent_welcome_title],


    subTitle = stringManager[R.string.dependent_welcome_subtitle],


    ctaLabel = stringManager[R.string.dependent_welcome_cta_label],


    )


    }


    }
    Molecule Presenter

    View Slide

  62. @Test fun models() = runTest {


    presenter.test {


    assertThat(awaitItem()).isEqualTo(


    DependentWelcomeViewModel(


    toolbarTitle = stringManager[R.string.investing_tab_title],


    title = stringManager[R.string.dependent_welcome_title],


    subTitle = stringManager[R.string.dependent_welcome_subtitle],


    ctaLabel = stringManager[R.string.dependent_welcome_cta_label],


    )


    )


    }


    }


    @Test fun `cta clicks navigates starts invest teen request authorization flow`() = runTest {


    presenter.test {


    awaitItem() // Default model emitted.


    sendEvent(CtaClicked)


    assertThat(navigator.awaitNextScreen()).isEqualTo(InvestingHome())


    }


    }
    Molecule Presenter Test: Turbine


    https://github.com/cashapp/turbine

    View Slide

  63. class SomethingStateManager(


    ) {


    @Composable


    fun somethingStates(): SomethingState {


    }


    }
    Composable Producer

    View Slide

  64. class SomethingStateManager(


    database: CashDatabase,


    private val ioContext: CoroutineContext,


    ) {


    private val somethingQueries = database.somethingStateQueries


    @Composable


    fun somethingStates(): SomethingState {


    val dbSomethingState: Investing_state? by remember {


    somethingQueries.select().asFlow().mapToOne(ioContext)


    }.collectAsState(null)


    if (dbSomethingState == null) return Loading


    return Content(


    hasOneThing = dbSomethingState!!.has_one_thing,


    )


    }


    }
    Composable Producer

    View Slide

  65. Navigation

    View Slide

  66. interface Screen : Parcelable
    @Parcelize


    data class StockDetails(


    val investmentEntityToken: InvestmentEntityToken,


    ) : Screen

    View Slide

  67. interface Navigator {


    fun goTo(screen: Screen)


    }
    LaunchedE
    f
    fect(events) {


    events.collect { event ->


    when (event) {


    CtaClicked -> { // Start flow. }


    Close -> { // Navigate. }


    }


    }


    }


    View Slide

  68. interface Navigator {


    fun goTo(screen: Screen)


    }
    LaunchedE
    f
    fect(events) {


    events.collect { event ->


    when (event) {


    CtaClicked -> { // Start flow. }


    Close -> {


    navigator.goTo(SomeScreen())


    }


    }


    }


    }


    View Slide

  69. Broadway

    View Slide

  70. Factory

    View Slide

  71. interface ViewFactory {


    fun createView(


    screen: Screen,


    parent: ViewGroup,


    ): Ui<*, *>?


    }
    interface PresenterFactory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    ): Presenter<*, *>?


    }

    View Slide

  72. Presenter Factory
    fun ObservableTransformer.asPresenter()


    : Presenter {


    return object : Presenter {


    override fun start(


    scope: CoroutineScope,


    ): Presenter.Binding {


    // Stuff.


    }


    }


    }

    View Slide

  73. fun CoroutinePresenter.asPresenter()


    : Presenter {


    return object : Presenter {


    override fun start(


    scope: CoroutineScope,


    ): Presenter.Binding {


    // Stuff.


    }


    }


    }
    Presenter Factory

    View Slide

  74. fun MoleculePresenter.asPresenter()


    : Presenter {


    return object : Presenter {


    override fun start(


    scope: CoroutineScope,


    ): Presenter.Binding {


    // Stuff.


    }


    }


    }
    Presenter Factory

    View Slide

  75. fun ObservableTransformer.asPresenter(): Presenter


    fun CoroutinePresenter.asPresenter(): Presenter


    fun MoleculePresenter.asPresenter(): Presenter
    Presenter Factory

    View Slide

  76. interface ViewFactory {


    fun createView(


    screen: Screen,


    parent: ViewGroup,


    ): Ui<*, *>?


    }
    interface PresenterFactory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    ): Presenter<*, *>?


    }

    View Slide

  77. 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,


    back: Boolean,


    isTab: (Screen) -> Boolean,


    ): Animator?


    }

    View Slide

  78. 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


    }


    }

    View Slide

  79. 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).asPresenter()


    else -> null


    }


    }


    }

    View Slide

  80. Glue

    View Slide

  81. @Provides


    fun provideBroadway(


    viewFactories: Set,


    presenterFactories: Set,


    ): Broadway {


    return Broadway(


    viewFactories = viewFactories.toList(),


    presenterFactories = presenterFactories.toList(),


    )


    }
    Glue

    View Slide

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


    }


    }
    Glue

    View Slide

  83. - Hook up your factories into Broadway.


    - Draw the rest of the owl.
    Glue: try it at home!

    View Slide

  84. Glue: try it at Cash App!
    - 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

    View Slide

  85. FIN

    View Slide

  86. References
    • Molecule


    • https://github.com/cashapp/molecule


    • Turbine


    • https://github.com/cashapp/turbine


    • Paparazzi


    • https://github.com/cashapp/paparazzi


    • Android Jetpack Compose


    • https://developer.android.com/jetpack/compose


    • Kotlin Coroutines


    • https://kotlinlang.org/docs/coroutines-overview.html


    • TestParameterInjector


    • https://github.com/google/TestParameterInjector


    • RxJava


    • https://github.com/ReactiveX/RxJava


    • Circuit


    • https://github.com/slackhq/circuit
    FIN

    View Slide