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

Reactive Workflows

rjrjr
September 26, 2017

Reactive Workflows

Our MVP app is too big. Introducing the Workflow pattern to try to get it back under control. Plays well with Rx, minimizes platform-specific dependencies. Woo hoo!

https://droidcon-server.herokuapp.com/showSession/103028
https://www.youtube.com/watch?v=KjoMnsc2lPo

rjrjr

September 26, 2017
Tweet

More Decks by rjrjr

Other Decks in Technology

Transcript

  1. View Slide

  2. reacting to code sprawl
    Reactive
    Workflows
    Ray Ryan

    View Slide

  3. Key-based MVP
    the legacy

    View Slide

  4. Key-based MVP
    Key
    flow.set(Key)

    View Slide

  5. Key-based MVP
    Key
    View

    View Slide

  6. Key-based MVP
    Key
    Presenter
    View

    View Slide

  7. Key-based MVP
    Key
    Presenter
    View
    Model Model Model

    View Slide

  8. Key-based MVP
    CartScreen
    Cart
    Presenter
    CartView
    Item
    Library
    Cart

    View Slide

  9. Key-based MVP
    CartScreen
    Cart
    Presenter
    CartView
    Item
    Library
    Cart
    flow.set(ChargeScreen)

    View Slide

  10. ChargeScreen
    Key-based MVP
    CartScreen
    Cart
    Presenter
    CartView
    Item
    Library
    Cart

    View Slide

  11. Key-based MVP
    ChargeScreen
    CartScreen

    View Slide

  12. Key-based MVP
    Charge
    Presenter
    Charge
    View
    Cart Bill
    Card
    Reader
    ChargeScreen
    CartScreen

    View Slide

  13. Key-based MVP
    Charge
    Presenter
    Charge
    View
    Cart Bill
    Card
    Reader
    ChargeScreen
    CartScreen

    View Slide

  14. Key-based MVP
    Charge
    Presenter
    Charge
    View
    Cart Bill
    Card
    Reader
    ChargeScreen
    CartScreen

    View Slide

  15. Nav logic sprawl
    ChargeScreen
    CartScreen

    View Slide

  16. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen

    View Slide

  17. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen

    View Slide

  18. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen

    View Slide

  19. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen
    ReceiptScreen

    View Slide

  20. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen
    ReceiptScreen

    View Slide

  21. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen
    ReceiptScreen

    View Slide

  22. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen
    ReceiptScreen

    View Slide

  23. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen
    ReceiptScreen
    Payment
    Runner

    View Slide

  24. Nav logic sprawl
    ChargeScreen
    CartScreen TipScreen
    SignatureScreen
    ReceiptScreen
    Payment
    Runner
    SettingsScreen

    View Slide

  25. Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner

    View Slide

  26. Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner
    cardSwiped()

    View Slide

  27. Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    cardSwiped()

    View Slide

  28. Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    cardSwiped()
    next()

    View Slide

  29. Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    cardSwiped()
    show tip screen?
    next()

    View Slide

  30. show signature screen?
    Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    cardSwiped()
    show tip screen?
    next()

    View Slide

  31. show signature screen?
    Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    finishPayment()
    cardSwiped()
    show tip screen?
    next()

    View Slide

  32. show signature screen?
    Nav & biz logic entangled
    Charge
    Presenter
    Bill
    Card
    Reader
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    finishPayment()
    cardSwiped()
    show tip screen?
    next()

    View Slide

  33. AddCustomer
    Runner
    Awkward reuse
    Cart
    Presenter
    CartScreen
    Cart

    View Slide

  34. AddCustomer
    Runner
    Awkward reuse
    Cart
    Presenter
    start()
    CartScreen
    Cart

    View Slide

  35. PickCustomerScreen
    AddCustomer
    Runner
    Awkward reuse
    Cart
    Presenter
    start()
    CartScreen
    Cart

    View Slide

  36. AddCustomer
    Runner
    Awkward reuse
    Pick
    Customer
    Presenter
    PickCustomerScreen
    CartScreen
    Cart

    View Slide

  37. AddCustomer
    Runner
    Awkward reuse
    Pick
    Customer
    Presenter
    PickCustomerScreen
    CartScreen
    Cart
    setCustomer()
    + Customer

    View Slide

  38. AddCustomer
    Runner
    Awkward reuse
    Receipt
    Presenter
    ReceiptScreen
    Receipt

    View Slide

  39. AddCustomer
    Runner
    Awkward reuse
    Receipt
    Presenter
    start()
    ReceiptScreen
    Receipt
    flow.set(PickCustomerScreen)

    View Slide

  40. AddCustomer
    Runner
    Awkward reuse
    Receipt
    Presenter
    start()
    ReceiptScreen
    Receipt
    PickCustomerScreen

    View Slide

  41. AddCustomer
    Runner
    Awkward reuse
    Pick
    Customer
    Presenter
    PickCustomerScreen
    ReceiptScreen

    View Slide

  42. AddCustomer
    Runner
    Awkward reuse
    Pick
    Customer
    Presenter
    PickCustomerScreen
    ReceiptScreen
    Cart Receipt
    ?

    View Slide

  43. AddCustomer
    Runner
    Awkward reuse
    Pick
    Customer
    Presenter
    PickCustomerScreen
    ReceiptScreen
    ?
    + Customer

    View Slide

  44. AddCustomer
    Runner
    Awkward reuse
    Pick
    Customer
    Presenter
    PickCustomerScreen
    ReceiptScreen
    ? + Customer

    View Slide

  45. Still, pretty good enough
    Nearly two hundred contributors
    Around three hundred screens
    Ships every two weeks

    View Slide

  46. Good enough don’t scale
    four years later

    View Slide

  47. Good enough don’t scale
    12 minute clean build
    2-4 minute rebuild
    Spaghetti of the commons

    View Slide

  48. iOS even worse
    ViewControllers full of business logic
    WWAD syndrome
    • Static singletons
    • CoreData all the things
    • Undiffable XIB, Storyboard files

    View Slide

  49. Learned helplessness
    Each platform > 750,000 LOC
    Remember, THREE HUNDRED SCREENS
    Nearly half just in payment flow
    Grassroots heroics no longer practical

    View Slide

  50. Learned helplessness
    Each platform > 750,000 LOC
    Remember, THREE HUNDRED SCREENS
    Nearly half just in payment flow
    Grassroots heroics no longer practical

    View Slide

  51. Reclaiming the commons
    we bring glad tidings from the future

    View Slide

  52. Reclaiming the commons
    Make fixing things someone’s job
    Make success possible
    • Define the problems
    • Describe utopia
    • Declare and execute two year plan
    Define the problems

    View Slide

  53. Define the problems
    Edit, compile, debug loop is too slow
    The code is too complex
    Dependencies are too interconnected

    View Slide

  54. Define the problems
    Edit, compile, debug loop is too slow
    The code is too complex
    Dependencies are too interconnected
    Modularize: small code blocks build faster
    Modularize: big features from simpler parts
    Modularize: dependencies are a DAG

    View Slide

  55. Describe Utopia

    View Slide

  56. Describe Utopia

    View Slide

  57. Utopia's core principals
    Immutability is assumed
    Be reactive: push, don’t pull
    Natural separation of UI and business concerns
    Uniformity of API across platforms
    Uniformity
    Utopia

    View Slide

  58. “Uniformity?” Code sharing?
    Maybe
    Writing is the easy part, maintenance is forever
    Shared code is foreign code
    Have you considered…
    • Shared tests
    • Shared API definitions (protocol buffers)
    Uniformity

    View Slide

  59. “Uniformity?” Code sharing?
    Maybe
    Writing is the easy part, maintenance is forever
    Shared code is foreign code
    Have you considered…
    • Shared tests
    • Shared API definitions (protocol buffers)
    Uniformity

    View Slide

  60. Workflows and Bricks
    Brick

    View Slide

  61. Workflows and Bricks
    Bricks
    Business model
    i/o
    Brick

    View Slide

  62. Workflows and Bricks
    Bricks
    Workflows
    Business model
    i/o
    App logic
    UI modeling
    Brick

    View Slide

  63. Workflows and Bricks
    Bricks
    Workflows
    Container
    Business model
    i/o
    App logic
    UI modeling
    Rendering
    UI event handling
    View
    Factory
    Brick

    View Slide

  64. Workflows and Bricks
    Bricks
    Workflows
    Container
    Business model
    i/o
    App logic
    UI modeling
    Rendering
    UI event handling
    Platform
    neutral*
    View code
    View
    Factory
    Brick

    View Slide

  65. “Brick” rhymes with “schmego”
    Brick

    View Slide

  66. “Brick” rhymes with “schmego”

    View Slide

  67. “Brick” rhymes with “schmego”

    View Slide

  68. “Brick” rhymes with “schmego”
    setFoo(FooValue f) Observable bar();

    View Slide

  69. “Brick” rhymes with “schmego”
    setFoo(FooValue f) Observable bar();

    View Slide

  70. “Brick” rhymes with “schmego”
    BarValue getBar();
    setFoo(FooValue f) Observable bar();

    View Slide

  71. “Brick” rhymes with “schmego”
    BarValue getBar();
    setFoo(FooValue f) Observable bar();

    View Slide

  72. “Brick” rhymes with “schmego”
    BarValue getBar();
    setFoo(FooValue f) Observable bar();
    SqlBrite / SqlDelight
    *RxPreferences, *Rooms
    Retrofit

    View Slide

  73. Build big bricks from small bricks

    View Slide

  74. Build big bricks from small bricks

    View Slide

  75. Build big bricks from small bricks

    View Slide

  76. Build big bricks from small bricks
    j2Objc JavaScript

    View Slide

  77. enum class StateOfPlay {
    PLAYING, VICTORY, DRAW
    }

    View Slide

  78. enum class StateOfPlay {
    PLAYING, VICTORY, DRAW
    }
    enum class MARK {
    EMPTY, X, O
    }

    View Slide

  79. enum class StateOfPlay {
    PLAYING, VICTORY, DRAW
    }
    enum class MARK {
    EMPTY, X, O
    }
    data class Player(
    val id: String,
    val name: String
    )

    View Slide

  80. data class GameState(
    val id: String,
    val playerX: Player,
    val playerO: Player,
    val stateOfPlay: StateOfPlay,
    val grid: List>,
    val activePlayerId: String
    )
    enum class StateOfPlay {
    PLAYING, VICTORY, DRAW
    }
    enum class MARK {
    EMPTY, X, O
    }
    data class Player(
    val id: String,
    val name: String
    )

    View Slide

  81. class GameRunner {
    fun newGame(xPlayerName: String, oPlayerName: String) {}
    fun restoreGame(clientId: String) {}
    fun takeSquare(row: Int, col: Int) {}
    fun end() {}
    fun gameState(): Observable {}
    }

    View Slide

  82. class GameRunner {
    /** @throws AssertionError if not called from main thread */
    fun newGame(xPlayerName: String, oPlayerName: String) {}
    /** @throws AssertionError if not called from main thread */
    fun restoreGame(clientId: String) {}
    /** @throws AssertionError if not called from main thread */
    fun takeSquare(row: Int, col: Int) {}
    /** @throws AssertionError if not called from main thread */
    fun end() {}
    fun gameState(): Observable {}
    }
    fun newGame(xPlayerName: String, oPlayerName: String)
    fun restoreGame(clientId: String)
    class GameRunner {

    View Slide

  83. class GameRunner {
    /** @throws AssertionError if not called from main thread */
    fun newGame(xPlayerName: String, oPlayerName: String) {}
    /** @throws AssertionError if not called from main thread */
    fun restoreGame(clientId: String) {}
    /** @throws AssertionError if not called from main thread */
    fun takeSquare(row: Int, col: Int) {}
    /** @throws AssertionError if not called from main thread */
    fun end() {}
    fun gameState(): Observable {}
    }
    fun newGame(xPlayerName: String, oPlayerName: String)
    fun restoreGame(clientId: String)
    class GameRunner {
    observeOn(mainThread())
    speakerdeck.com/rjrjr/
    where-the-reactive-rubber-meets-the-road

    View Slide

  84. class GameRunner {
    /** @throws AssertionError if not called from main thread */
    fun newGame(xPlayerName: String, oPlayerName: String) {}
    /** @throws AssertionError if not called from main thread */
    fun restoreGame(clientId: String) {}
    /** @throws AssertionError if not called from main thread */
    fun takeSquare(row: Int, col: Int) {}
    /** @throws AssertionError if not called from main thread */
    fun end() {}
    fun gameState(): Observable {}
    }
    fun newGame(xPlayerName: String, oPlayerName: String)
    fun restoreGame(clientId: String)
    class GameRunner {

    View Slide

  85. class GameRunner {

    View Slide

  86. class GameRunner {
    sealed class Command {
    data class NewGame(val xPlayer: String, val yPlayer: String): Command()
    data class RestoreGame(val id: String): Command()
    data class TakeSquare(val row: Int, val col: Int): Command()
    object End: Command()
    }
    fun asTransformer():
    Observable.Transformer {
    }
    }

    View Slide

  87. class GameRunner {
    sealed class Command {
    data class NewGame(val xPlayer: String, val yPlayer: String): Command()
    data class RestoreGame(val id: String): Command()
    data class TakeSquare(val row: Int, val col: Int): Command()
    object End: Command()
    }
    fun asTransformer():
    Observable.Transformer {
    }
    }
    search for:
    Dan Lew transformer

    View Slide

  88. Workflow through the brickyard

    View Slide

  89. Remember these things?
    Charge
    Presenter
    Bill
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    finishPayment()
    next()
    flow.set(TipScreen)
    Charge
    View

    View Slide

  90. Remember these things?
    Charge
    Presenter
    Bill
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    finishPayment()
    next()
    flow.set(TipScreen)
    Charge
    View

    View Slide

  91. Remember these things?
    Charge
    Presenter
    Bill
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    finishPayment()
    next()
    flow.set(TipScreen)
    Charge
    View

    View Slide

  92. Remember these things?
    Charge
    Presenter
    Bill
    ChargeScreen
    Payment
    Runner
    setCardInfo()
    finishPayment()
    next()
    flow.set(TipScreen)
    Charge
    View

    View Slide

  93. Presenter
    Runner + = Workflow

    View Slide

  94. Workflow as
    Presenter
    + = Workflow
    start(I input) Maybe result()
    abort()
    pipeline

    View Slide

  95. Workflow as
    = Workflow
    start(I input) Maybe result()
    abort()
    ui driver
    Observable screen()

    View Slide

  96. Rhymes with “view model”
    /** Allows interaction with a [Workflow] in a particular state. */
    abstract class WorkflowScreen protected constructor(
    /** Uniquely identifies this screen. */
    val key: String,
    /** Stream of data to render this screen. */
    val screenData: Observable,
    /** Callback methods (click handlers, etc.) handled by this screen. */
    val eventHandler: E
    )

    View Slide

  97. LoginScreen AuthorizingScreen SecondFactorScreen

    View Slide

  98. class LoginScreen(
    errorMessage: Observable,
    eventHandler: Events
    ) : WorkflowScreen(KEY, errorMessage, eventSink) {
    companion object {
    val KEY = LoginScreen::class.name
    }
    interface Events {
    fun onLogin(event: SubmitLogin)
    }
    data class SubmitLogin(
    val email: String,
    val password: String
    )
    }

    View Slide

  99. class LoginScreen(
    errorMessage: Observable,
    eventHandler: Events
    ) : WorkflowScreen(KEY, errorMessage, eventSink) {
    companion object {
    val KEY = LoginScreen::class.name
    }
    interface Events {
    fun onLogin(event: SubmitLogin)
    }
    data class SubmitLogin(
    val email: String,
    val password: String
    )
    }

    View Slide

  100. class LoginScreen(
    errorMessage: Observable,
    eventHandler: Events
    ) : WorkflowScreen(KEY, errorMessage, eventSink) {
    companion object {
    val KEY = LoginScreen::class.name
    }
    interface Events {
    fun onLogin(event: SubmitLogin)
    }
    data class SubmitLogin(
    val email: String,
    val password: String
    )
    }

    View Slide

  101. class LoginScreen(
    errorMessage: Observable,
    eventHandler: Events
    ) : WorkflowScreen(KEY, errorMessage, eventSink) {
    companion object {
    val KEY = LoginScreen::class.name
    }
    interface Events {
    fun onLogin(event: SubmitLogin)
    }
    data class SubmitLogin(
    val email: String,
    val password: String
    )
    }

    View Slide

  102. class ConfirmChargeCardOnFileScreen(
    screenData: Observable,
    eventHandler: Events
    ) : WorkflowScreen(KEY, screenData, eventHandler) {
    companion object {
    val KEY = ConfirmChargeCardOnFileScreen::class.name
    }
    data class ScreenData(
    val amountDue: Money,
    val customerName: String,
    val cardNameAndNumber: String,
    val instrumentIndex: Int)
    interface Events {
    fun doNotChargeCardOnFile()
    fun chargeCardOnFile(tenderedAmount: Money, instrumentIndex: Int)
    }

    View Slide

  103. Workflow as
    = Workflow
    start(I input) Maybe result()
    abort()
    view model source
    Observable screen()

    View Slide

  104. Workflow as
    = Workflow
    start(I input) Maybe result()
    abort()
    view model source
    Observable screen() ✔

    View Slide

  105. View
    Factory
    Container
    Workflow as
    = Workflow
    start(I input) Maybe result()
    abort()
    view model source
    Observable screen()
    RootView

    View Slide

  106. class AuthViewFactory : AbstractViewFactory(asList(
    bindLayout(LoginScreen.KEY, R.layout.login) { screen ->
    LoginCoordinator(screen as LoginScreen)
    },
    bindLayout(AuthorizingScreen.KEY, R.layout.authorizing) { screen ->
    AuthorizingCoordinator(screen as AuthorizingScreen)
    },
    bindLayout(SecondFactorScreen.KEY, R.layout.second_factor) { screen ->
    SecondFactorCoordinator(screen as SecondFactorScreen)
    }
    ))

    View Slide

  107. class LoginCoordinator(private val screen: LoginScreen) : Coordinator() {
    private var subscription: Subscription = Subscriptions.unsubscribed()
    override fun attach(view: View) {

    View Slide

  108. class LoginCoordinator(private val screen: LoginScreen) : Coordinator() {
    private var subscription: Subscription = Subscriptions.unsubscribed()
    override fun attach(view: View) {
    val error = view.findViewById(R.id.login_error_message) as TextView
    val email = view.findViewById(R.id.login_email) as EditText
    val password = view.findViewById(R.id.login_password) as EditText
    val button = view.findViewById(R.id.login_button) as Button

    View Slide

  109. class LoginCoordinator(private val screen: LoginScreen) : Coordinator() {
    private var subscription: Subscription = Subscriptions.unsubscribed()
    override fun attach(view: View) {
    val error = view.findViewById(R.id.login_error_message) as TextView
    val email = view.findViewById(R.id.login_email) as EditText
    val password = view.findViewById(R.id.login_password) as EditText
    val button = view.findViewById(R.id.login_button) as Button
    button.setOnClickListener { _ ->
    val event = SubmitLogin(email.text.toString(),
    password.text.toString())
    screen.eventHandler.login(event)
    }

    View Slide

  110. class LoginCoordinator(private val screen: LoginScreen) : Coordinator() {
    private var subscription: Subscription = Subscriptions.unsubscribed()
    override fun attach(view: View) {
    val error = view.findViewById(R.id.login_error_message) as TextView
    val email = view.findViewById(R.id.login_email) as EditText
    val password = view.findViewById(R.id.login_password) as EditText
    val button = view.findViewById(R.id.login_button) as Button
    button.setOnClickListener { _ ->
    val event = SubmitLogin(email.text.toString(),
    password.text.toString())
    screen.eventHandler.login(event)
    }
    subscription = screen.screenData.subscribe { error.text = it }
    }

    View Slide

  111. class LoginCoordinator(private val screen: LoginScreen) : Coordinator() {
    private var subscription: Subscription = Subscriptions.unsubscribed()
    override fun attach(view: View) {

    }
    override fun detach(view: View?) {
    subscription.unsubscribe()
    }

    View Slide

  112. class LoginCoordinator(private val screen: LoginScreen) : Coordinator() {
    private var subscription: Subscription = Subscriptions.unsubscribed()
    override fun attach(view: View) {

    }
    override fun detach(view: View?) {
    subscription.unsubscribe()
    }
    search for:
    square coordinators

    View Slide

  113. Workflow as
    = Workflow
    start(I input) Maybe result()
    abort()
    state machine
    Container
    Observable screen()
    View
    Factory
    RootView

    View Slide

  114. Workflow as
    = Workflow
    start(I input) Maybe result()
    abort()
    state machine
    Container
    Observable screen()
    View
    Factory
    RootView


    + Square Coordinators

    View Slide

  115. Workflow as
    = Workflow
    start(I input) Maybe result()
    abort()
    state machine
    Container
    Observable screen()
    View
    Factory
    RootView


    + Square Coordinators

    View Slide

  116. class AuthWorkflow(): Workflow

    View Slide

  117. class AuthWorkflow(): Workflow,
    LoginScreen.Events, SecondFactorScreen.Events

    View Slide

  118. class AuthWorkflow(): Workflow,
    LoginScreen.Events, SecondFactorScreen.Events {
    private val currentScreen = BehaviorSubject.create()

    View Slide

  119. class AuthWorkflow(): Workflow,
    LoginScreen.Events, SecondFactorScreen.Events {
    private val currentScreen = BehaviorSubject.create()
    private val loginMessage = BehaviorSubject.create("")
    private val authorizingMessage = BehaviorSubject.create()
    private val secondFactorMessage = BehaviorSubject.create()

    View Slide

  120. class AuthWorkflow(): Workflow,
    LoginScreen.Events, SecondFactorScreen.Events {
    private val currentScreen = BehaviorSubject.create()
    private val loginMessage = BehaviorSubject.create("")
    private val authorizingMessage = BehaviorSubject.create()
    private val secondFactorMessage = BehaviorSubject.create()
    override fun screen(): Observable =
    currentScreen.map { it ->
    when (it) {
    LoginScreen.KEY -> LoginScreen(loginMessage, this)
    AuthorizingScreen.KEY -> AuthorizingScreen(authorizingMessage)
    SecondFactorScreen.KEY -> SecondFactorScreen(secondFactorMessage, this)
    else -> throw IllegalArgumentException("Unknown key " + it)
    }
    }

    View Slide

  121. class AuthWorkflow(): Workflow,
    LoginScreen.Events, SecondFactorScreen.Events {

    View Slide

  122. class AuthWorkflow(): Workflow,
    LoginScreen.Events, SecondFactorScreen.Events {

    override fun onLogin(event: LoginScreen.SubmitLogin) {
    stateMachine.onEvent(event)
    }
    override fun onSecondFactor(event: SecondFactorScreen.SecondFactor) {
    stateMachine.onEvent(event)
    }

    View Slide

  123. internal enum class State {
    LOGIN_PROMPT, AUTHORIZING, SECOND_FACTOR_PROMPT, DONE
    }

    View Slide

  124. internal enum class State {
    LOGIN_PROMPT, AUTHORIZING, SECOND_FACTOR_PROMPT, DONE
    }
    init {
    stateMachine = FiniteStateMachine(
    onEntry(AUTHORIZING) { currentScreen.onNext(AuthorizingScreen.KEY) },
    onEntry(SECOND_FACTOR_PROMPT) {
    currentScreen.onNext(SecondFactorScreen.KEY)
    },

    View Slide

  125. internal enum class State {
    LOGIN_PROMPT, AUTHORIZING, SECOND_FACTOR_PROMPT, DONE
    }
    init {
    stateMachine = FiniteStateMachine(

    View Slide

  126. internal enum class State {
    LOGIN_PROMPT, AUTHORIZING, SECOND_FACTOR_PROMPT, DONE
    }
    init {
    stateMachine = FiniteStateMachine(

    transition(LOGIN_PROMPT, SubmitLogin::class, AUTHORIZING)
    .doAction { doLogin(it) },
    transition(AUTHORIZING, AuthResponse::class, LOGIN_PROMPT)
    .onlyIf { isLoginFailure(it) }
    .doAction { response ->
    val errorMessage = response.errorMessage
    loginMessage.onNext(errorMessage)
    },

    View Slide

  127. internal enum class State {
    LOGIN_PROMPT, AUTHORIZING, SECOND_FACTOR_PROMPT, DONE
    }
    init {
    stateMachine = FiniteStateMachine(

    transition(LOGIN_PROMPT, SubmitLogin::class, AUTHORIZING)
    .doAction { doLogin(it) },
    transition(AUTHORIZING, AuthResponse::class, LOGIN_PROMPT)
    .onlyIf { isLoginFailure(it) }
    .doAction { response ->
    val errorMessage = response.errorMessage
    loginMessage.onNext(errorMessage)
    },
    search for:
    Andy Matuschak states

    View Slide

  128. NewGameScreen GamePlayScreen GameOverScreen
    ConfirmQuitScreen

    View Slide

  129. class TicTacToeViewFactory private constructor()
    : AbstractViewFactory(asList(
    bindLayout(NewGameScreen.KEY, layout.new_game_layout
    ) { screen -> NewGameCoordinator(screen as NewGameScreen) },
    bindLayout(GamePlayScreen.KEY, layout.game_play_layout
    ) { screen -> GamePlayCoordinator(screen as GamePlayScreen) },
    bindLayout(GameOverScreen.KEY, layout.game_play_layout
    ) { screen -> GameOverCoordinator(screen as GameOverScreen) },
    bindDialog(ConfirmQuitScreen.KEY
    ) { screen -> ConfirmQuitDialogFactory(screen as ConfirmQuitScreen) }
    ))

    View Slide

  130. class TicTacToeViewFactory private constructor()
    : AbstractViewFactory(asList(
    bindLayout(NewGameScreen.KEY, layout.new_game_layout
    ) { screen -> NewGameCoordinator(screen as NewGameScreen) },
    bindLayout(GamePlayScreen.KEY, layout.game_play_layout
    ) { screen -> GamePlayCoordinator(screen as GamePlayScreen) },
    bindLayout(GameOverScreen.KEY, layout.game_play_layout
    ) { screen -> GameOverCoordinator(screen as GameOverScreen) },
    bindDialog(ConfirmQuitScreen.KEY
    ) { screen -> ConfirmQuitDialogFactory(screen as ConfirmQuitScreen) }
    ))

    View Slide

  131. class TicTacToeViewFactory private constructor()
    : AbstractViewFactory(asList(
    bindLayout(NewGameScreen.KEY, layout.new_game_layout
    ) { screen -> NewGameCoordinator(screen as NewGameScreen) },
    bindLayout(GamePlayScreen.KEY, layout.game_play_layout
    ) { screen -> GamePlayCoordinator(screen as GamePlayScreen) },
    bindLayout(GameOverScreen.KEY, layout.game_play_layout
    ) { screen -> GameOverCoordinator(screen as GameOverScreen) },
    bindDialog(ConfirmQuitScreen.KEY
    ) { screen -> ConfirmQuitDialogFactory(screen as ConfirmQuitScreen) }
    ))

    View Slide

  132. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow

    View Slide

  133. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {

    View Slide

  134. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {
    private val gameStates: Observable =
    gameRunner.gameState().startWith(NO_GAME)

    View Slide

  135. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {
    private val gameStates: Observable =
    gameRunner.gameState().startWith(NO_GAME)
    companion object {
    private val FAKE_ID = UUID.randomUUID().toString()
    private val NO_GAME = TicTacToeGameState.newGame(FAKE_ID,
    Player(FAKE_ID, "X"), Player(FAKE_ID, "O"))
    }

    View Slide

  136. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {
    private val gameStates: Observable =
    gameRunner.gameState().startWith(NO_GAME)

    View Slide

  137. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {
    private val gameStates: Observable =
    gameRunner.gameState().startWith(NO_GAME)
    private val quitting = BehaviorSubject.create(false)

    View Slide

  138. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {
    private val gameStates: Observable =
    gameRunner.gameState().startWith(NO_GAME)
    private val quitting = BehaviorSubject.create(false)
    private val screen = combineLatest(gameStates, quitting,
    { gameState, quitting -> update(gameState, quitting) })
    .replay(1)
    override fun screen(): Observable> = screen

    View Slide

  139. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {

    View Slide

  140. class TicTacToeWorkflow(
    private val gameRunner: GameRunner
    ): Workflow,
    NewGameScreen.Events, GamePlayScreen.Events,
    ConfirmQuitScreen.Events, GameOverScreen.Events {

    private fun update(gameState: GameState, quitting: Boolean):
    WorkflowScreen<*,*> {
    if (quitting) return ConfirmQuitScreen(this)
    if (gameState == NO_GAME) NewGameScreen(this)
    return when (gameState.stateOfPlay) {
    PLAYING -> GamePlayScreen(gameStates, this)
    VICTORY, DRAW -> GameOverScreen(gameStates, this)
    }
    }

    View Slide

  141. RootView
    View
    Factory
    Container
    Workflow

    View Slide

  142. RootView
    View
    Factory
    Container
    Workflow

    View Slide

  143. RootView
    Key
    ScreenData
    Events
    WorkflowScreen
    View
    Factory
    Container
    Workflow

    View Slide

  144. RootView
    Key
    ScreenData
    Events
    WorkflowScreen
    View
    Factory
    Container
    Workflow

    View Slide

  145. RootView
    Key
    ScreenData
    Events
    WorkflowScreen
    View
    Factory
    Container
    Workflow

    View Slide

  146. RootView
    View
    Factory
    Container
    Workflow

    View Slide

  147. RootView
    Key
    ScreenData
    Events
    WorkflowScreen
    View
    Factory
    Container
    Workflow

    View Slide

  148. RootView
    View
    Factory
    Container
    Workflow

    View Slide

  149. Workflow
    You said these things compose

    View Slide

  150. Workflow
    You said these things compose
    Auth
    Workflow
    Game
    Workflow

    View Slide

  151. new CompositeWorkflow<>(
    // Start in the AuthWorkflow. When it finishes, kick off
    // a TicTacToe game.
    new WorkflowBinding<>(AuthWorkflow.class,
    () -> ignoreStartArg(authWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT))),
    // When a TicTacToe game ends, start another one.
    new WorkflowBinding<>(TicTacToeWorkflow.class,
    () -> ignoreStartArg(ticTacToeWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT)))
    )

    View Slide

  152. new CompositeWorkflow<>(
    // Start in the AuthWorkflow. When it finishes, kick off
    // a TicTacToe game.
    new WorkflowBinding<>(AuthWorkflow.class,
    () -> ignoreStartArg(authWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT))),
    // When a TicTacToe game ends, start another one.
    new WorkflowBinding<>(TicTacToeWorkflow.class,
    () -> ignoreStartArg(ticTacToeWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT)))
    )

    View Slide

  153. new CompositeWorkflow<>(
    // Start in the AuthWorkflow. When it finishes, kick off
    // a TicTacToe game.
    new WorkflowBinding<>(AuthWorkflow.class,
    () -> ignoreStartArg(authWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT))),
    // When a TicTacToe game ends, start another one.
    new WorkflowBinding<>(TicTacToeWorkflow.class,
    () -> ignoreStartArg(ticTacToeWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT)))
    )

    View Slide

  154. new CompositeWorkflow<>(
    // Start in the AuthWorkflow. When it finishes, kick off
    // a TicTacToe game.
    new WorkflowBinding<>(AuthWorkflow.class,
    () -> ignoreStartArg(authWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT))),
    // When a TicTacToe game ends, start another one.
    new WorkflowBinding<>(TicTacToeWorkflow.class,
    () -> ignoreStartArg(ticTacToeWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT)))
    )

    View Slide

  155. new CompositeWorkflow<>(
    // Start in the AuthWorkflow. When it finishes, kick off
    // a TicTacToe game.
    new WorkflowBinding<>(AuthWorkflow.class,
    () -> ignoreStartArg(authWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT))),
    // When a TicTacToe game ends, start another one.
    new WorkflowBinding<>(TicTacToeWorkflow.class,
    () -> ignoreStartArg(ticTacToeWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT)))
    )

    View Slide

  156. new CompositeWorkflow<>(
    // Start in the AuthWorkflow. When it finishes, kick off
    // a TicTacToe game.
    new WorkflowBinding<>(AuthWorkflow.class,
    () -> ignoreStartArg(authWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT))),
    // When a TicTacToe game ends, start another one.
    new WorkflowBinding<>(TicTacToeWorkflow.class,
    () -> ignoreStartArg(ticTacToeWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT)))
    )

    View Slide

  157. new CompositeWorkflow<>(
    // Start in the AuthWorkflow. When it finishes, kick off
    // a TicTacToe game.
    new WorkflowBinding<>(AuthWorkflow.class,
    () -> ignoreStartArg(authWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT))),
    // When a TicTacToe game ends, start another one.
    new WorkflowBinding<>(TicTacToeWorkflow.class,
    () -> ignoreStartArg(ticTacToeWorkflowProvider.get()),
    (composite, result) -> composite.startWorkflow(
    forArg(TicTacToeWorkflow.class, (Unit) UNIT)))
    )

    View Slide

  158. Master
    Status
    Detail1 Detail2 Detail3

    View Slide

  159. Tactics

    View Slide

  160. Step zero: make refactoring…
    …possible
    • Mock service layer
    • Robot-based UI tests (espresso, KIF)
    …tolerable
    • OkBuck for Android
    • CocoaPods for iOS

    View Slide

  161. Divide first, conquer later

    View Slide

  162. Always be writing
    PSAs
    Design docs (everyone, all the time)
    Policy docs (a few, mostly: “write a damn design doc”)
    How-to guides and sample code

    View Slide

  163. Full disclosure
    Brick pattern established on both
    First j2Objc brick entering production
    Non-toy workflows in development on both
    Composite Workflow on Android
    Composite WorkflowScreen TBD

    View Slide

  164. @rjrjr
    speakerdeck.com/rjrjr/where-the-reactive-rubber-meets-the-road
    Dan Lew transformer
    github.com/square/coordinators
    Andy Matuschak states (http://bfy.tw/E969)
    square.com/jobs

    View Slide