Slide 1

Slide 1 text

Ray Ryan Timothy Donnelly Reactive Workflows Update

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Workflow Business Objects Container

Slide 5

Slide 5 text

Workflow Container Business Objects CRUD

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

NewGame PlayGame QuitGame GameOver

Slide 14

Slide 14 text

New Game Screen Play Game Screen Quit Game Screen Game Over Screen

Slide 15

Slide 15 text

New Game Play Game Quit Game Game Over

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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 )

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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)

Slide 23

Slide 23 text

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)

Slide 24

Slide 24 text

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 )

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

New Game Quit Game Game Over Play Game

Slide 43

Slide 43 text

New Game PlayGameWorkflow Quit Game Game Over

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

New Game PlayGameWorkflow Quit Game Game Over

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

/** * @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 )

Slide 61

Slide 61 text

/** * @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<*, *>

Slide 62

Slide 62 text

/** * @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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Tim Time

Slide 67

Slide 67 text

Workflows + Swift

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

No content

Slide 70

Slide 70 text

No content

Slide 71

Slide 71 text

No content

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

No content

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

78 Adapting UIKit to a reactive world

Slide 79

Slide 79 text

79 Back Stack

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

No content

Slide 83

Slide 83 text

No content

Slide 84

Slide 84 text

No content

Slide 85

Slide 85 text

No content

Slide 86

Slide 86 text

No content

Slide 87

Slide 87 text

No content

Slide 88

Slide 88 text

88 Modal Container

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

90

Slide 91

Slide 91 text

91

Slide 92

Slide 92 text

92

Slide 93

Slide 93 text

93 Demo

Slide 94

Slide 94 text

94

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

96

Slide 97

Slide 97 text

97 ...and what about debugging tools?

Slide 98

Slide 98 text

98

Slide 99

Slide 99 text

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