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

Reactive Workflows Update

rjrjr
August 28, 2018

Reactive Workflows Update

rjrjr

August 28, 2018
Tweet

More Decks by rjrjr

Other Decks in Technology

Transcript

  1. Ray Ryan
    Timothy Donnelly
    Reactive Workflows Update

    View full-size slide

  2. Immutability is assumed
    Be reactive: push, don’t pull
    Natural separation of UI and business concerns
    Composition or go home
    Cardinal Architecture Virtues

    View full-size slide

  3. Workflow
    Business Objects Container

    View full-size slide

  4. Workflow Container
    Business Objects
    CRUD

    View full-size slide

  5. Workflow
    Navigation, View Model
    NewGame
    PlayGame
    QuitGame
    GameOver
    Business Objects
    CRUD
    Container

    View full-size slide

  6. Container
    Rendering, Event Handling
    Business Objects
    CRUD
    Workflow
    Navigation, View Model
    NewGame
    PlayGame
    QuitGame
    GameOver

    View full-size slide

  7. Business Objects
    CRUD
    State
    View Model
    Event
    Update
    Workflow
    Navigation, View Model
    NewGame
    PlayGame
    QuitGame
    GameOver
    Container
    Rendering, Event Handling

    View full-size slide

  8. val bar: BarValue
    fun setFoo(f: FooValue)
    fun setBar(b: BarValue)
    fun setBaz(b: Bazvalue)
    val bang: Observable
    val buzz: Observable

    View full-size slide

  9. fun perform(op: Operation)
    sealed class Operation {
    class Update(vararg stuff: Stuff) : Operation()
    class Delete(vararg ids: Int) : Operation()
    }
    val stuffs: Observable>

    View full-size slide

  10. fun perform(op: Operation) val stuffs: Observable>
    RxPreferences, Rooms
    SqlDelight
    Retrofit

    View full-size slide

  11. Container
    Rendering, Event Handling
    Business Objects
    CRUD
    Workflow
    Navigation, View Model
    NewGame
    PlayGame
    QuitGame
    GameOver
    State
    View Model
    Event
    Update

    View full-size slide

  12. NewGame
    PlayGame
    QuitGame
    GameOver

    View full-size slide

  13. New
    Game
    Screen
    Play
    Game
    Screen
    Quit
    Game
    Screen
    Game
    Over
    Screen

    View full-size slide

  14. New
    Game
    Play
    Game
    Quit
    Game
    Game
    Over

    View full-size slide

  15. Play Game
    X’s Turn
    Draw
    X Wins
    Quit
    O Wins
    O’s Turn

    View full-size slide

  16. Play Game
    Playing(
    X or O
    )
    Completed(
    X, O, draw or quit
    )

    View full-size slide

  17. Play Game
    Playing(
    Symbol
    )
    Completed(
    Ending
    )
    Symbol {
    X,
    O,
    None
    }
    Ending {
    Victor(Symbol)
    Draw
    Quitter(Symbol)
    }

    View full-size slide

  18. Play Game
    Playing(
    Symbol
    )
    Completed(
    Ending
    )
    Event {
    TakeSquare(row, col)
    Quit
    }
    Symbol {
    X,
    O,
    None
    }
    Ending {
    Victor(Symbol)
    Draw
    Quitter(Symbol)
    }

    View full-size slide

  19. Play Game
    Playing(
    Turn
    )
    Completed(
    Ending,
    Turn
    )
    typealias Board = List>
    data class Turn(
    val players: Map,
    val playing: Symbol = X,
    val board: Board = EMPTY_BOARD
    )

    View full-size slide

  20. interface Workflow {
    val state: Observable
    val result: Single
    fun abandon()
    }

    View full-size slide

  21. class PlayGameWorkflow(initial: State) : Workflow {
    sealed class Event {
    class TakeSquare(
    val row: Int,
    val col: Int
    ) : Event()
    object Quit : Event()
    }
    private val events = PublishRelay.create()
    fun sendEvent(e: Event) = events.call(e)

    View full-size slide

  22. class PlayGameWorkflow(initial: State) : Workflow {
    sealed class Event {
    class TakeSquare(
    val row: Int,
    val col: Int
    ) : Event()
    object Quit : Event()
    }
    private val events = PublishRelay.create()
    fun sendEvent(e: Event) = events.call(e)

    View full-size slide

  23. class PlayGameWorkflow(initial: State) : Workflow {
    sealed class State {
    abstract val turn: Turn
    data class Playing(
    override val turn: Turn
    ) : State()
    data class Completed(
    val ending: Ending,
    override val turn: Turn
    ) : State()
    }
    data class Turn(
    val players: Map,
    val playing: Symbol = X,
    val board: Board = EMPTY_BOARD
    )

    View full-size slide

  24. class PlayGameWorkflow(initial: State) : Workflow {
    sealed class State {
    abstract val turn: Turn
    data class Playing(
    override val turn: Turn
    ) : State()
    data class Completed(
    val ending: Ending,
    override val turn: Turn
    ) : State()
    }

    View full-size slide

  25. class PlayGameWorkflow(initial: State) : Workflow {
    sealed class State {
    abstract val turn: Turn
    data class Playing(
    override val turn: Turn
    ) : State()
    data class Completed(
    val ending: Ending,
    override val turn: Turn
    ) : State()
    }
    sealed class Ending {
    object Victor : Ending()
    object Draw : Ending()
    object Quitter : Ending()
    }

    View full-size slide

  26. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  27. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  28. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  29. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  30. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  31. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  32. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  33. class PlayGameWorkflow(initial: State) : Workflow {
    private val states = BehaviorRelay.create(Single.just(initial))
    private val _state: ConnectableObservable = states
    .switchMap { it.toObservable() }
    .doOnNext { currentState ->
    states.call(nextState(currentState))
    }
    .replay(1)
    override val state: Observable =
    _state.takeWhile { it !is Completed }
    .map { it.turn }
    override val result: Single =
    _state.ofType()
    .first()
    .toSingle()
    .cache()

    View full-size slide

  34. class PlayGameWorkflow(initial: State) : Workflow {
    private fun nextState(fromState: State): Single = when (fromState) {
    is Completed -> Observable.never()
    is Playing -> events.first().map { event ->
    when (event) {
    is TakeSquare -> takeSquareAndNextState(fromState.turn, event.row, event.col)
    Quit -> Completed(Quitter, fromState.turn)
    }
    }
    }.toSingle()

    View full-size slide

  35. class PlayGameReactor : Reactor() {
    override fun onReact(fromState: Playing):
    Single> = selectEvent {
    onEvent {
    EnterState(takeSquareAndNextState(fromState.turn, it.row, it.col))
    }
    onEvent { FinishWith(Completed(Quitter, fromState.turn)) }
    }

    View full-size slide

  36. class PlayGameReactor : Reactor() {
    override fun onReact(fromState: Playing):
    Single> = selectEvent {
    onEvent {
    EnterState(takeSquareAndNextState(fromState.turn, it.row, it.col))
    }
    onEvent { FinishWith(Completed(Quitter, fromState.turn)) }
    }

    View full-size slide

  37. class PlayGameReactor : Reactor() {
    override fun onReact(fromState: Playing):
    Single> = selectEvent {
    onEvent {
    EnterState(takeSquareAndNextState(fromState.turn, it.row, it.col))
    }
    onEvent { FinishWith(Completed(Quitter, fromState.turn)) }
    }

    View full-size slide

  38. class PlayGameReactor : Reactor() {
    override fun onReact(fromState: Playing):
    Single> = selectEvent {
    onEvent {
    EnterState(takeSquareAndNextState(fromState.turn, it.row, it.col))
    }
    onEvent { FinishWith(Completed(Quitter, fromState.turn)) }
    }

    View full-size slide

  39. class PlayGameReactor : Reactor() {
    typealias PlayGameWorkflow = ReactorWorkflow
    companion object {
    fun startWorkflow(
    x: String,
    o: String
    ): PlayGameWorkflow =
    PlayGameReactor().asWorkflow(Playing(Turn(x, o)))
    }

    View full-size slide

  40. class PlayGameReactor : Reactor() {
    typealias PlayGameWorkflow = ReactorWorkflow
    companion object {
    fun startWorkflow(
    x: String,
    o: String
    ): PlayGameWorkflow =
    PlayGameReactor().asWorkflow(Playing(Turn(x, o)))
    fun resumeWorkflow(turn: Turn): PlayGameWorkflow =
    PlayGameReactor().asWorkflow(Playing(turn))
    }

    View full-size slide

  41. New
    Game
    Quit
    Game
    Game
    Over
    Play
    Game

    View full-size slide

  42. New
    Game
    PlayGameWorkflow
    Quit
    Game
    Game
    Over

    View full-size slide

  43. class GameAppReactor : Reactor() {
    sealed class State {
    class NewGame(
    val defaultXName: String = "X",
    val defaultOName: String = "O"
    ) : State()
    class Playing(val workflow: PlayGameWorkflow) : State()
    class MaybeQuitting(val completedGame: Completed) : State()
    class GameOver(val completedGame: Completed) : State()
    }

    View full-size slide

  44. class GameAppReactor : Reactor() {
    sealed class Event {
    class StartGame(
    val x: String,
    val o: String
    ) : Event()
    object ConfirmQuit : Event()
    object ContinuePlaying : Event()
    object StartNewGame : Event()
    object QuitApp : Event()
    }

    View full-size slide

  45. class GameAppReactor : Reactor() {
    override fun onReact(state: State):
    Single> = when (state) {

    View full-size slide

  46. class GameAppReactor : Reactor() {
    override fun onReact(state: State):
    Single> = when (state) {
    is NewGame -> selectEvent {
    onEvent { FinishWith(Unit) }
    onEvent {
    EnterState(Playing(PlayGameReactor.startWorkflow(it.x, it.o)))
    }
    }

    View full-size slide

  47. class GameAppReactor : Reactor() {
    override fun onReact(state: State):
    Single> = when (state) {
    is NewGame -> selectEvent {
    onEvent { FinishWith(Unit) }
    onEvent {
    EnterState(Playing(PlayGameReactor.startWorkflow(it.x, it.o)))
    }
    }

    View full-size slide

  48. class GameAppReactor : Reactor() {
    override fun onReact(state: State):
    Single> = when (state) {
    is NewGame -> selectEvent {
    onEvent { FinishWith(Unit) }
    onEvent {
    EnterState(Playing(PlayGameReactor.startWorkflow(it.x, it.o)))
    }
    }
    is Playing -> state.workflow.result.map { result ->
    when (result.ending) {
    Quitter -> EnterState(MaybeQuitting(result))
    else -> EnterState(GameOver(result))
    }
    }

    View full-size slide

  49. class GameAppReactor : Reactor() {
    override fun onReact(state: State):
    Single> = when (state) {
    is Playing -> state.workflow.result.map { result ->
    when (result.ending) {
    Quitter -> EnterState(MaybeQuitting(result))
    else -> EnterState(GameOver(result))
    }
    }
    is MaybeQuitting -> selectEvent {
    onEvent { EnterState(GameOver(state.completedGame)) }
    onEvent {
    EnterState(Playing(PlayGameReactor.resumeWorkflow(state.completedGame.turn)))
    }
    }

    View full-size slide

  50. class GameAppReactor : Reactor() {
    override fun onReact(state: State):
    Single> = when (state) {
    is Playing -> state.workflow.result.map { result ->
    when (result.ending) {
    Quitter -> EnterState(MaybeQuitting(result))
    else -> EnterState(GameOver(result))
    }
    }
    is MaybeQuitting -> selectEvent {
    onEvent { EnterState(GameOver(state.completedGame)) }
    onEvent {
    EnterState(Playing(PlayGameReactor.resumeWorkflow(state.completedGame.turn)))
    }
    }

    View full-size slide

  51. class GameAppReactor : Reactor() {
    override fun onReact(state: State):
    Single> = when (state) {
    is Playing -> state.workflow.result.map { result ->
    when (result.ending) {
    Quitter -> EnterState(MaybeQuitting(result))
    else -> EnterState(GameOver(result))
    }
    }
    is MaybeQuitting -> selectEvent {
    onEvent { EnterState(GameOver(state.completedGame)) }
    onEvent {
    EnterState(Playing(PlayGameReactor.resumeWorkflow(state.completedGame.turn)))
    }
    }

    View full-size slide

  52. New
    Game
    PlayGameWorkflow
    Quit
    Game
    Game
    Over

    View full-size slide

  53. Playing(PlayGameWorkflow)
    NewGame
    GameOver(Completed)
    MaybeQuitting(Completed)

    View full-size slide

  54. Game
    Service
    Playing(PlayGameWorkflow)
    NewGame
    MaybeQuitting(Completed)
    ConfirmQuit
    Continue
    Playing
    Quitter
    !Quitter
    StartNewGame
    GameOver(Completed)
    StartNewGame

    View full-size slide

  55. Game
    Service
    Playing(PlayGameWorkflow)
    NewGame
    GameOver(
    Completed,
    SyncState
    )
    MaybeQuitting(Completed)
    ConfirmQuit
    Continue
    Playing
    Quitter
    !Quitter
    StartNewGame
    StartNewGame

    View full-size slide

  56. override fun onReact(state: State):
    Single> = when (state) {
    // Left as an exercise for the reader.
    //
    // Hint: Retrofit requests are rx.Single<*>

    View full-size slide

  57. Container
    Rendering, Event Handling
    Business Objects
    CRUD
    Workflow
    Navigation, View Model
    NewGame
    PlayGame
    QuitGame
    GameOver
    State
    View Model
    Event
    Update

    View full-size slide

  58. Workflow
    Navigation, View Model
    Container
    Rendering, Event Handling
    Business Objects
    CRUD
    ?

    View full-size slide

  59. /**
    * @param data A complete rendering of this screen.
    * @param eventHandler The object that accepts events for this screen.
    */
    class Screen(
    val data: D,
    val eventHandler: E
    )

    View full-size slide

  60. /**
    * @param data A complete rendering of this screen.
    * @param eventHandler The object that accepts events for this screen.
    */
    class Screen(
    val data: D,
    val eventHandler: E
    )
    typealias AnyScreen = Screen<*, *>

    View full-size slide

  61. /**
    * @param data A complete rendering of this screen.
    * @param eventHandler The object that accepts events for this screen.
    */
    class Screen(
    val data: D,
    val eventHandler: E
    )
    typealias AnyScreen = Screen<*, *>
    /**
    * A [Workflow] that exposes its state as a stream
    * of [screen models][Screen].
    */
    typealias ScreenWorkflow = Workflow

    View full-size slide

  62. class GameAppScreens(private val wrapped: GameAppWorkflow) :
    ScreenWorkflow {
    override val state: Observable =
    wrapped.state.switchMap {
    when (it) {
    is Playing -> it.workflow.state.map { playing ->
    Screen(playing.turn, it.workflow)
    }
    else -> just(Screen(it, wrapped))
    }
    }
    override val result: Single = wrapped.result
    override fun abandon() = wrapped.abandon()
    }

    View full-size slide

  63. class GameAppScreens(private val wrapped: GameAppWorkflow) :
    ScreenWorkflow {
    override val state: Observable =
    wrapped.state.switchMap {
    when (it) {
    is Playing -> it.workflow.state.map { playing ->
    Screen(playing.turn, it.workflow)
    }
    else -> just(Screen(it, wrapped))
    }
    }
    override val result: Single = wrapped.result
    override fun abandon() = wrapped.abandon()
    }

    View full-size slide

  64. class GameAppScreens(private val wrapped: GameAppWorkflow) :
    ScreenWorkflow {
    override val state: Observable =
    wrapped.state.switchMap {
    when (it) {
    is Playing -> it.workflow.state
    else -> just(Screen(it, wrapped))
    }
    }
    override val result: Single = wrapped.result
    override fun abandon() = wrapped.abandon()
    }

    View full-size slide

  65. Workflows + Swift

    View full-size slide

  66. public struct ViewRegistry {
    // ...
    public func provideView(for screen: T) -> View
    // ...
    }
    The View Registry turns screen models into live views.

    View full-size slide

  67. public struct ViewRegistry {
    // ...
    public func provideView(for screen: T) -> View
    // Registers a factory block for the screen type `T`.
    public mutating func register(
    screenType: T.Type,
    factory: @escaping (Observable, ViewRegistry) -> View
    // ...
    }
    The View Registry requires screens to be registered.
    73

    View full-size slide

  68. public struct ViewRegistry {
    // ...
    public func provideView(for screen: T) -> View
    // Registers a factory block for the screen type `T`.
    public mutating func register(
    screenType: T.Type,
    factory: @escaping (Observable, ViewRegistry) -> View
    // ...
    }
    The View Registry is recursive.
    74

    View full-size slide

  69. 78
    Adapting UIKit to a reactive world

    View full-size slide

  70. 79
    Back Stack

    View full-size slide

  71. public struct BackStackScreen: Screen {
    public var currentItem: Item
    public struct Item {
    public var screen: Screen
    public var key: String
    public var barContent: NavigationBarContent
    }
    }

    View full-size slide

  72. 88
    Modal Container

    View full-size slide

  73. public struct ModalContainerScreen: Screen {
    public var main: Screen
    public var modal: Screen?
    }
    89

    View full-size slide

  74. 95
    What else can we do with a fully decoupled UI layer?

    View full-size slide

  75. 97
    ...and what about debugging tools?

    View full-size slide

  76. square.com
    Ray Ryan / octodon.social/@rjrjr
    Timothy Donnelly / twitter.com/tdonnelly
    github.com/JakeWharton/RxRelay
    github.com/square/coordinators

    View full-size slide