Reactive Workflows

82ee6bce819efe5b9cc6c51dea03e8da?s=47 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

82ee6bce819efe5b9cc6c51dea03e8da?s=128

rjrjr

September 26, 2017
Tweet

Transcript

  1. None
  2. reacting to code sprawl Reactive Workflows Ray Ryan

  3. Key-based MVP the legacy

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

  5. Key-based MVP Key View

  6. Key-based MVP Key Presenter View

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

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

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

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

  11. Key-based MVP ChargeScreen CartScreen

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

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

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

    ChargeScreen CartScreen
  15. Nav logic sprawl ChargeScreen CartScreen

  16. Nav logic sprawl ChargeScreen CartScreen TipScreen

  17. Nav logic sprawl ChargeScreen CartScreen TipScreen SignatureScreen

  18. Nav logic sprawl ChargeScreen CartScreen TipScreen SignatureScreen

  19. Nav logic sprawl ChargeScreen CartScreen TipScreen SignatureScreen ReceiptScreen

  20. Nav logic sprawl ChargeScreen CartScreen TipScreen SignatureScreen ReceiptScreen

  21. Nav logic sprawl ChargeScreen CartScreen TipScreen SignatureScreen ReceiptScreen

  22. Nav logic sprawl ChargeScreen CartScreen TipScreen SignatureScreen ReceiptScreen

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

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

    SettingsScreen
  25. Nav & biz logic entangled Charge Presenter Bill Card Reader

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

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

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

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

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

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

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

    Bill Card Reader ChargeScreen Payment Runner setCardInfo() finishPayment() cardSwiped() show tip screen? next()
  33. AddCustomer Runner Awkward reuse Cart Presenter CartScreen Cart

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

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

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

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

    setCustomer() + Customer
  38. AddCustomer Runner Awkward reuse Receipt Presenter ReceiptScreen Receipt

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

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

  41. AddCustomer Runner Awkward reuse Pick Customer Presenter PickCustomerScreen ReceiptScreen

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

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

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

    + Customer
  45. Still, pretty good enough Nearly two hundred contributors Around three

    hundred screens Ships every two weeks
  46. Good enough don’t scale four years later

  47. Good enough don’t scale 12 minute clean build 2-4 minute

    rebuild Spaghetti of the commons
  48. iOS even worse ViewControllers full of business logic WWAD syndrome

    • Static singletons • CoreData all the things • Undiffable XIB, Storyboard files
  49. Learned helplessness Each platform > 750,000 LOC Remember, THREE HUNDRED

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

    SCREENS Nearly half just in payment flow Grassroots heroics no longer practical
  51. Reclaiming the commons we bring glad tidings from the future

  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
  53. Define the problems Edit, compile, debug loop is too slow

    The code is too complex Dependencies are too interconnected
  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
  55. Describe Utopia

  56. Describe Utopia

  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
  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
  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
  60. Workflows and Bricks Brick

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

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

    UI modeling Brick
  63. Workflows and Bricks Bricks Workflows Container Business model i/o App

    logic UI modeling Rendering UI event handling View Factory Brick
  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
  65. “Brick” rhymes with “schmego” Brick

  66. “Brick” rhymes with “schmego”

  67. “Brick” rhymes with “schmego”

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

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

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

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

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

    SqlBrite / SqlDelight *RxPreferences, *Rooms Retrofit
  73. Build big bricks from small bricks

  74. Build big bricks from small bricks

  75. Build big bricks from small bricks

  76. Build big bricks from small bricks j2Objc JavaScript

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

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

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

    MARK { EMPTY, X, O } data class Player( val id: String, val name: String )
  80. data class GameState( val id: String, val playerX: Player, val

    playerO: Player, val stateOfPlay: StateOfPlay, val grid: List<List<MARK>>, val activePlayerId: String ) enum class StateOfPlay { PLAYING, VICTORY, DRAW } enum class MARK { EMPTY, X, O } data class Player( val id: String, val name: String )
  81. class GameRunner { fun newGame(xPlayerName: String, oPlayerName: String) {} fun

    restoreGame(clientId: String) {} fun takeSquare(row: Int, col: Int) {} fun end() {} fun gameState(): Observable<GameState> {} }
  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<GameState> {} } fun newGame(xPlayerName: String, oPlayerName: String) fun restoreGame(clientId: String) class GameRunner {
  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<GameState> {} } fun newGame(xPlayerName: String, oPlayerName: String) fun restoreGame(clientId: String) class GameRunner { observeOn(mainThread()) speakerdeck.com/rjrjr/ where-the-reactive-rubber-meets-the-road
  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<GameState> {} } fun newGame(xPlayerName: String, oPlayerName: String) fun restoreGame(clientId: String) class GameRunner {
  85. class GameRunner {

  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<Command, GameState> { } }
  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<Command, GameState> { } } search for: Dan Lew transformer
  88. Workflow through the brickyard

  89. Remember these things? Charge Presenter Bill ChargeScreen Payment Runner setCardInfo()

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

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

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

    finishPayment() next() flow.set(TipScreen) Charge View
  93. Presenter Runner + = Workflow<I, R>

  94. Workflow as Presenter + = Workflow<I, R> start(I input) Maybe<R>

    result() abort() pipeline
  95. Workflow as = Workflow<I, R> start(I input) Maybe<R> result() abort()

    ui driver Observable<WorkflowScreen> screen()
  96. Rhymes with “view model” /** Allows interaction with a [Workflow]

    in a particular state. */ abstract class WorkflowScreen<D, out E> protected constructor( /** Uniquely identifies this screen. */ val key: String, /** Stream of data to render this screen. */ val screenData: Observable<D>, /** Callback methods (click handlers, etc.) handled by this screen. */ val eventHandler: E )
  97. LoginScreen AuthorizingScreen SecondFactorScreen

  98. class LoginScreen( errorMessage: Observable<String>, eventHandler: Events ) : WorkflowScreen<String, Events>(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 ) }
  99. class LoginScreen( errorMessage: Observable<String>, eventHandler: Events ) : WorkflowScreen<String, Events>(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 ) }
  100. class LoginScreen( errorMessage: Observable<String>, eventHandler: Events ) : WorkflowScreen<String, Events>(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 ) }
  101. class LoginScreen( errorMessage: Observable<String>, eventHandler: Events ) : WorkflowScreen<String, Events>(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 ) }
  102. class ConfirmChargeCardOnFileScreen( screenData: Observable<ScreenData>, eventHandler: Events ) : WorkflowScreen<ScreenData, Events>(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) }
  103. Workflow as = Workflow<I, R> start(I input) Maybe<R> result() abort()

    view model source Observable<WorkflowScreen> screen()
  104. Workflow as = Workflow<I, R> start(I input) Maybe<R> result() abort()

    view model source Observable<WorkflowScreen> screen() ✔
  105. View Factory Container Workflow as = Workflow<I, R> start(I input)

    Maybe<R> result() abort() view model source Observable<WorkflowScreen> screen() RootView ✔
  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) } ))
  107. class LoginCoordinator(private val screen: LoginScreen) : Coordinator() { private var

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

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

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

    subscription: Subscription = Subscriptions.unsubscribed() override fun attach(view: View) { val error = view.findViewById<View>(R.id.login_error_message) as TextView val email = view.findViewById<View>(R.id.login_email) as EditText val password = view.findViewById<View>(R.id.login_password) as EditText val button = view.findViewById<View>(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 } }
  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() }
  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
  113. Workflow as = Workflow<I, R> start(I input) Maybe<R> result() abort()

    state machine Container Observable<WorkflowScreen> screen() View Factory RootView ✔
  114. Workflow as = Workflow<I, R> start(I input) Maybe<R> result() abort()

    state machine Container Observable<WorkflowScreen> screen() View Factory RootView ✔ ✔ + Square Coordinators
  115. Workflow as = Workflow<I, R> start(I input) Maybe<R> result() abort()

    state machine Container Observable<WorkflowScreen> screen() View Factory RootView ✔ ✔ + Square Coordinators
  116. class AuthWorkflow(): Workflow<Unit, String>

  117. class AuthWorkflow(): Workflow<Unit, String>, LoginScreen.Events, SecondFactorScreen.Events

  118. class AuthWorkflow(): Workflow<Unit, String>, LoginScreen.Events, SecondFactorScreen.Events { private val currentScreen

    = BehaviorSubject.create<String>()
  119. class AuthWorkflow(): Workflow<Unit, String>, LoginScreen.Events, SecondFactorScreen.Events { private val currentScreen

    = BehaviorSubject.create<String>() private val loginMessage = BehaviorSubject.create("") private val authorizingMessage = BehaviorSubject.create<String>() private val secondFactorMessage = BehaviorSubject.create<String>()
  120. class AuthWorkflow(): Workflow<Unit, String>, LoginScreen.Events, SecondFactorScreen.Events { private val currentScreen

    = BehaviorSubject.create<String>() private val loginMessage = BehaviorSubject.create("") private val authorizingMessage = BehaviorSubject.create<String>() private val secondFactorMessage = BehaviorSubject.create<String>() override fun screen(): Observable<WorkflowScreen<*,*> = 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) } }
  121. class AuthWorkflow(): Workflow<Unit, String>, LoginScreen.Events, SecondFactorScreen.Events { …

  122. class AuthWorkflow(): Workflow<Unit, String>, LoginScreen.Events, SecondFactorScreen.Events { … override fun

    onLogin(event: LoginScreen.SubmitLogin) { stateMachine.onEvent(event) } override fun onSecondFactor(event: SecondFactorScreen.SecondFactor) { stateMachine.onEvent(event) }
  123. internal enum class State { LOGIN_PROMPT, AUTHORIZING, SECOND_FACTOR_PROMPT, DONE }

  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) },
  125. internal enum class State { LOGIN_PROMPT, AUTHORIZING, SECOND_FACTOR_PROMPT, DONE }

    init { stateMachine = FiniteStateMachine( …
  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) },
  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
  128. NewGameScreen GamePlayScreen GameOverScreen ConfirmQuitScreen

  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) } ))
  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) } ))
  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) } ))
  132. class TicTacToeWorkflow( private val gameRunner: GameRunner ): Workflow<Unit, TicTacToeGameState>

  133. class TicTacToeWorkflow( private val gameRunner: GameRunner ): Workflow<Unit, TicTacToeGameState>, NewGameScreen.Events,

    GamePlayScreen.Events, ConfirmQuitScreen.Events, GameOverScreen.Events {
  134. class TicTacToeWorkflow( private val gameRunner: GameRunner ): Workflow<Unit, TicTacToeGameState>, NewGameScreen.Events,

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

    GamePlayScreen.Events, ConfirmQuitScreen.Events, GameOverScreen.Events { private val gameStates: Observable<TicTacToeGameState> = 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")) }
  136. class TicTacToeWorkflow( private val gameRunner: GameRunner ): Workflow<Unit, TicTacToeGameState>, NewGameScreen.Events,

    GamePlayScreen.Events, ConfirmQuitScreen.Events, GameOverScreen.Events { private val gameStates: Observable<TicTacToeGameState> = gameRunner.gameState().startWith(NO_GAME) …
  137. class TicTacToeWorkflow( private val gameRunner: GameRunner ): Workflow<Unit, TicTacToeGameState>, NewGameScreen.Events,

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

    GamePlayScreen.Events, ConfirmQuitScreen.Events, GameOverScreen.Events { private val gameStates: Observable<TicTacToeGameState> = 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<WorkflowScreen<*,*>> = screen …
  139. class TicTacToeWorkflow( private val gameRunner: GameRunner ): Workflow<Unit, TicTacToeGameState>, NewGameScreen.Events,

    GamePlayScreen.Events, ConfirmQuitScreen.Events, GameOverScreen.Events { …
  140. class TicTacToeWorkflow( private val gameRunner: GameRunner ): Workflow<Unit, TicTacToeGameState>, 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) } } …
  141. RootView View Factory Container Workflow

  142. RootView View Factory Container Workflow

  143. RootView Key ScreenData Events WorkflowScreen View Factory Container Workflow

  144. RootView Key ScreenData Events WorkflowScreen View Factory Container Workflow

  145. RootView Key ScreenData Events WorkflowScreen View Factory Container Workflow

  146. RootView View Factory Container Workflow

  147. RootView Key ScreenData Events WorkflowScreen View Factory Container Workflow

  148. RootView View Factory Container Workflow

  149. Workflow You said these things compose

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

  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))) )
  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))) )
  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))) )
  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))) )
  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))) )
  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))) )
  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))) )
  158. Master Status Detail1 Detail2 Detail3

  159. Tactics

  160. Step zero: make refactoring… …possible • Mock service layer •

    Robot-based UI tests (espresso, KIF) …tolerable • OkBuck for Android • CocoaPods for iOS
  161. Divide first, conquer later

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