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

A State Container Architecture for mobile applications

A State Container Architecture for mobile applications

David González

June 25, 2018
Tweet

More Decks by David González

Other Decks in Programming

Transcript

  1. A State Container based
    approach to
    Architecting Mobile
    applications
    David González
    @dggonzalez
    [email protected]

    View full-size slide

  2. Beacon Mobile SDK

    View full-size slide

  3. Beacon Mobile SDK

    View full-size slide

  4. Beacon Mobile SDK

    View full-size slide

  5. Beacon Mobile SDK

    View full-size slide

  6. Page heading
    Motivation
    Explore a common (and
    shared) architecture
    language (and code)
    between iOS and Android

    View full-size slide

  7. Page heading
    Three traits of good design
    Balanced
    distribution of
    responsibilities

    View full-size slide

  8. Page heading
    Three traits of good design
    Testability

    View full-size slide

  9. Page heading
    Three traits of good design
    Easy to use

    View full-size slide

  10. Good code is the one
    that you can easily
    delete
    - David González

    View full-size slide

  11. Page heading
    Motivation
    Greenfield

    View full-size slide

  12. Page heading
    Motivation
    Brownfield

    View full-size slide

  13. A good design is easier
    to change than a bad
    design
    - Dave Thomas

    View full-size slide

  14. Beacon Mobile SDK

    View full-size slide

  15. Page heading
    The assembly problem
    let viewController = UIViewController()
    let presenter = Presenter(viewController, model)
    viewController.presenter = presenter

    View full-size slide

  16. Conflictive states
    Invisible outputs
    - MVP: Command from
    presenter to View
    - MVVM: Observable property
    No traceability
    Hard to follow the course of events”
    - showLoading(), showData(),
    refresh(), showMoreData()

    View full-size slide

  17. Page heading
    Motivation
    Null references
    The billion dollar mistake

    View full-size slide

  18. A State Container
    approach

    View full-size slide

  19. Page heading
    Implementation
    View State

    View full-size slide

  20. Page heading
    ViewState
    sealed class ReplyViewState : ViewState() {
    data class Form(val formOptions: BeaconContactForm,
    val message: String,
    val attachments: Map,
    val formValid: Boolean,
    val draft: String) : ReplyViewState()
    object SendingReply : ReplyViewState()
    object ReplySent : ReplyViewState()
    class SendReplyError(error: Throwable) : ViewState.Error(error)
    }

    View full-size slide

  21. Page heading
    ViewState
    sealed class ReplyViewState : ViewState() {
    data class Form(val formOptions: BeaconContactForm,
    val message: String,
    val attachments: Map,
    val formValid: Boolean,
    val draft: String) : ReplyViewState()
    object SendingReply : ReplyViewState()
    object ReplySent : ReplyViewState()
    class SendReplyError(error: Throwable) : ViewState.Error(error)
    }

    View full-size slide

  22. Page heading
    ViewState
    sealed class ReplyViewState : ViewState() {
    data class Form(val formOptions: BeaconContactForm,
    val message: String,
    val attachments: Map,
    val formValid: Boolean,
    val draft: String) : ReplyViewState()
    object SendingReply : ReplyViewState()
    object ReplySent : ReplyViewState()
    class SendReplyError(error: Throwable) : ViewState.Error(error)
    }

    View full-size slide

  23. Page heading
    ViewState
    sealed class ReplyViewState : ViewState() {
    data class Form(val formOptions: BeaconContactForm,
    val message: String,
    val attachments: Map,
    val formValid: Boolean,
    val draft: String) : ReplyViewState()
    object SendingReply : ReplyViewState()
    object ReplySent : ReplyViewState()
    class SendReplyError(error: Throwable) : ViewState.Error(error)
    }

    View full-size slide

  24. Page heading
    ViewState
    sealed class ReplyViewState : ViewState() {
    data class Form(val formOptions: BeaconContactForm,
    val message: String,
    val attachments: Map,
    val formValid: Boolean,
    val draft: String) : ReplyViewState()
    object SendingReply : ReplyViewState()
    object ReplySent : ReplyViewState()
    class SendReplyError(error: Throwable) : ViewState.Error(error)
    }

    View full-size slide

  25. Page heading
    Pattern matching
    override fun render(state: ViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  26. Page heading
    Pattern matching
    override fun render(state: ViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  27. Page heading
    Pattern matching
    override fun render(state: ViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  28. Page heading
    Pattern matching
    override fun render(state: ViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  29. Page heading
    Pattern matching
    override fun render(state: ViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  30. Page heading
    Pattern matching
    override fun render(state: ViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  31. Page heading
    Implementation
    UI as a State
    Machine

    View full-size slide

  32. Page heading
    UI as a State Machine
    sealed class ViewState {
    object Idle : ViewState()
    object Loading : ViewState()
    object Empty : ViewState()
    open class Error(val exception: Throwable) : ViewState()
    fun toLoading(viewState: ViewState): ViewState {
    return when(viewState){
    is Loading -> Error(Throwable("Wrong state transition"))
    else -> Loading
    }
    }
    }

    View full-size slide

  33. Page heading
    UI as a State Machine
    sealed class ViewState {
    object Idle : ViewState()
    object Loading : ViewState()
    object Empty : ViewState()
    open class Error(val exception: Throwable) : ViewState()
    fun toLoading(viewState: ViewState): ViewState {
    return when(viewState){
    is Loading -> Error(Throwable("Wrong state transition"))
    else -> Loading
    }
    }
    }

    View full-size slide

  34. Page heading
    UI as a State Machine
    sealed class ViewState {
    object Idle : ViewState()
    object Loading : ViewState()
    object Empty : ViewState()
    open class Error(val exception: Throwable) : ViewState()
    fun toLoading(viewState: ViewState): ViewState {
    return when(viewState){
    is Loading -> Error(Throwable("Wrong state transition"))
    else -> Loading
    }
    }
    }

    View full-size slide

  35. Page heading
    UI as a State Machine
    sealed class ViewState {
    object Idle : ViewState()
    object Loading : ViewState()
    object Empty : ViewState()
    open class Error(val exception: Throwable) : ViewState()
    fun toLoading(viewState: ViewState): ViewState {
    return when(viewState){
    is Loading -> Error(Throwable("Wrong state transition"))
    else -> Loading
    }
    }
    }

    View full-size slide

  36. Page heading
    Implementation
    Action (Command)
    Identifies actions from UI layer

    View full-size slide

  37. Page heading
    Implementation
    sealed class SendMessageAction : BeaconAction() {
    object Cleanup : SendMessageAction()
    object SelectAttachment : SendMessageAction()
    object InitialAction : SendMessageAction()
    object LoadForm : SendMessageAction()
    data class SendMessage(val formValues: FormValues) : SendMessageAction()
    data class ValidateForm(val formValues: FormValues) : SendMessageAction()
    }

    View full-size slide

  38. Page heading
    Implementation
    sealed class SendMessageAction : BeaconAction() {
    object Cleanup : SendMessageAction()
    object SelectAttachment : SendMessageAction()
    object InitialAction : SendMessageAction()
    object LoadForm : SendMessageAction()
    data class SendMessage(val formValues: FormValues) : SendMessageAction()
    data class ValidateForm(val formValues: FormValues) : SendMessageAction()
    }

    View full-size slide

  39. Page heading
    Implementation
    sealed class SendMessageAction : BeaconAction() {
    object Cleanup : SendMessageAction()
    object SelectAttachment : SendMessageAction()
    object InitialAction : SendMessageAction()
    object LoadForm : SendMessageAction()
    data class SendMessage(val formValues: FormValues) : SendMessageAction()
    data class ValidateForm(val formValues: FormValues) : SendMessageAction()
    }

    View full-size slide

  40. Page heading
    Implementation
    ViewModel

    View full-size slide

  41. Page heading
    ViewModel
    abstract class BaseViewModel : ViewModel() {
    val state = MediatorLiveData()
    protected fun stateDidChange(viewState: ViewState){
    state.value = viewState
    }
    abstract fun interpret(action: Action)
    }

    View full-size slide

  42. Page heading
    ViewModel
    abstract class BaseViewModel : ViewModel() {
    val state = MediatorLiveData()
    protected fun stateDidChange(viewState: ViewState){
    state.value = viewState
    }
    abstract fun interpret(action: Action)
    }

    View full-size slide

  43. Page heading
    ViewModel
    abstract class BaseViewModel : ViewModel() {
    val state = MediatorLiveData()
    protected fun stateDidChange(viewState: ViewState){
    state.value = viewState
    }
    abstract fun interpret(action: Action)
    }

    View full-size slide

  44. Page heading
    ViewModel
    abstract class BaseViewModel : ViewModel() {
    val state = MediatorLiveData()
    protected fun stateDidChange(viewState: ViewState){
    state.value = viewState
    }
    abstract fun interpret(action: Action)
    }

    View full-size slide

  45. Page heading
    ViewModel
    class MoviesViewModel: BaseViewModel() {
    init {
    stateDidChange(ViewState.Idle)

    }
    }

    View full-size slide

  46. Page heading
    Implementation
    The glue
    Observing the view state from the Activity

    View full-size slide

  47. Page heading
    Implementation
    viewModel.viewState.observe(this, Observer {
    it?.let { render(it) }
    })
    override fun render(state: BeaconViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  48. Page heading
    Implementation
    viewModel.viewState.observe(this, Observer {
    it?.let { render(it) }
    })
    override fun render(state: BeaconViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  49. Page heading
    Implementation
    viewModel.viewState.observe(this, Observer {
    it?.let { render(it) }
    })
    override fun render(state: BeaconViewState) {
    when (state) {
    is ReplyViewState.Form -> renderForm(state)
    is ReplyViewState.ReplySent -> renderReplySent()
    is ReplyViewState.MissingMessage -> renderMissingMessage()
    is ReplyViewState.SendingReply -> renderLoading()
    is ReplyViewState.SendReplyError -> renderError(state)
    }
    }

    View full-size slide

  50. Page heading
    Implementation
    ViewState vs DomainState

    View full-size slide

  51. Page heading
    Implementation
    App = UI(state)

    View full-size slide

  52. Page heading
    Implementation
    UI(viewState(domainState))

    View full-size slide

  53. Page heading
    Implementation
    Store (Reducers)

    View full-size slide

  54. Page heading
    The missing piece
    interface Store {
    fun reduce(action: Action)
    fun subscribe() LiveData
    }

    View full-size slide

  55. Page heading
    The missing piece
    sealed class DomainState {
    object LoadingMovies : DomainState()
    data class MoviesLoaded(movies: List)
    }

    View full-size slide

  56. Page heading
    The missing piece
    class MoviesViewModel(private val store: MoviesStore) : BaseViewModel() {
    init {
    stateDidChange(ViewState.Idle)
    state.addSource(store.subscribe(), {
    when (it) {
    is DomainState.LoadingMovies -> stateDidChange(ViewState.Loading)
    }
    })
    }
    override fun interpret(action: Action) {
    store.reduce(action)
    }
    }

    View full-size slide

  57. Page heading
    The missing piece
    class MoviesViewModel(private val store: MoviesStore) : BaseViewModel() {
    init {
    stateDidChange(ViewState.Idle)
    state.addSource(store.subscribe(), {
    when (it) {
    is DomainState.LoadingMovies -> stateDidChange(ViewState.Loading)
    }
    })
    }
    override fun interpret(action: Action) {
    store.reduce(action)
    }
    }

    View full-size slide

  58. Page heading
    The missing piece
    class MoviesViewModel(private val store: MoviesStore) : BaseViewModel() {
    init {
    stateDidChange(ViewState.Idle)
    state.addSource(store.subscribe(), {
    when (it) {
    is DomainState.LoadingMovies -> stateDidChange(ViewState.Loading)
    }
    })
    }
    override fun interpret(action: Action) {
    store.reduce(action)
    }
    }

    View full-size slide

  59. Page heading
    The missing piece
    class MoviesViewModel(private val store: MoviesStore) : BaseViewModel() {
    init {
    stateDidChange(ViewState.Idle)
    state.addSource(store.subscribe(), {
    when (it) {
    is DomainState.LoadingMovies -> stateDidChange(ViewState.Loading)
    }
    })
    }
    override fun interpret(action: Action) {
    store.reduce(action)
    }
    }

    View full-size slide

  60. Page heading
    The missing piece
    class MoviesViewModel(private val store: MoviesStore) : BaseViewModel() {
    init {
    stateDidChange(ViewState.Idle)
    state.addSource(store.subscribe(), {
    when (it) {
    is DomainState.LoadingMovies -> stateDidChange(ViewState.Loading)
    }
    })
    }
    override fun interpret(action: Action) {
    store.reduce(action)
    }
    }

    View full-size slide

  61. Page heading
    The missing piece

    View full-size slide

  62. Page heading
    The missing piece
    Functional Use Cases

    View full-size slide

  63. Page heading
    The missing piece
    abstract class UseCase where Type : Any {
    abstract suspend fun run(params: Params): Either
    fun execute(onResult: (Either) -> Unit, params: Params) {
    val job = async(CommonPool) { run(params) }
    launch(UI) { onResult.invoke(job.await()) }
    }
    class None
    }

    View full-size slide

  64. Page heading
    The missing piece
    abstract class UseCase where Type : Any {
    abstract suspend fun run(params: Params): Either
    fun execute(onResult: (Either) -> Unit, params: Params) {
    val job = async(CommonPool) { run(params) }
    launch(UI) { onResult.invoke(job.await()) }
    }
    class None
    }

    View full-size slide

  65. Page heading
    The missing piece
    abstract class UseCase where Type : Any {
    abstract suspend fun run(params: Params): Either
    fun execute(onResult: (Either) -> Unit, params: Params) {
    val job = async(CommonPool) { run(params) }
    launch(UI) { onResult.invoke(job.await()) }
    }
    class None
    }

    View full-size slide

  66. Page heading
    The missing piece
    abstract class UseCase where Type : Any {
    abstract suspend fun run(params: Params): Either
    fun execute(onResult: (Either) -> Unit, params: Params) {
    val job = async(CommonPool) { run(params) }
    launch(UI) { onResult.invoke(job.await()) }
    }
    class None
    }

    View full-size slide

  67. Page heading
    The missing piece
    abstract class UseCase where Type : Any {
    abstract suspend fun run(params: Params): Either
    fun execute(onResult: (Either) -> Unit, params: Params) {
    val job = async(CommonPool) { run(params) }
    launch(UI) { onResult.invoke(job.await()) }
    }
    class None
    }

    View full-size slide

  68. Page heading
    The missing piece
    abstract class UseCase where Type : Any {
    abstract suspend fun run(params: Params): Either
    fun execute(onResult: (Either) -> Unit, params: Params) {
    val job = async(CommonPool) { run(params) }
    launch(UI) { onResult.invoke(job.await()) }
    }
    class None
    }

    View full-size slide

  69. Page heading
    The missing piece
    abstract class UseCase where Type : Any {
    abstract suspend fun run(params: Params): Either
    fun execute(onResult: (Either) -> Unit, params: Params) {
    val job = async(CommonPool) { run(params) }
    launch(UI) { onResult.invoke(job.await()) }
    }
    class None
    }

    View full-size slide

  70. Page heading
    The missing piece
    class GetMovies(private val repository: MoviesRepository) :
    UseCase() {
    override suspend fun run(params: None): Either {
    return repository.getAllMovies()
    }
    }

    View full-size slide

  71. Page heading
    The missing piece
    Error Handling

    View full-size slide

  72. Page heading
    The missing piece
    sealed class Either {
    data class Left(val a: L) : Either()
    data class Right(val b: R) : Either()
    val isRight get() = this is Right
    val isLeft get() = this is Left
    fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any =
    when (this) {
    is Either.Left -> fnL(a)
    is Either.Right -> fnR(b)
    }
    fun flatMap(fn: (R) -> Either): Either {...}
    fun map(fn: (R) -> (T)): Either {...}
    }

    View full-size slide

  73. Page heading
    The missing piece
    sealed class Failure {
    class NetworkConnection: Failure()
    class ServerError: Failure()
    /** Feature specific failures */
    abstract class FeatureFailure: Failure()
    }

    View full-size slide

  74. Page heading
    The missing piece
    sealed class Failure {
    class NetworkConnection: Failure()
    class ServerError: Failure()
    /** Feature specific failures */
    abstract class FeatureFailure: Failure()
    }

    View full-size slide

  75. Page heading
    The missing piece
    sealed class Failure {
    class NetworkConnection: Failure()
    class ServerError: Failure()
    /** Feature specific failures */
    abstract class FeatureFailure: Failure()
    }

    View full-size slide

  76. Page heading
    The missing piece
    sealed class Failure {
    class NetworkConnection: Failure()
    class ServerError: Failure()
    /** Feature specific failures */
    abstract class FeatureFailure: Failure()
    }

    View full-size slide

  77. Page heading
    The missing piece
    private fun handleFailure(failure: Failure?) {
    when (failure) {
    is NetworkConnection -> renderFailure(R.string.failure_network)
    is ServerError -> renderFailure(R.string.failure_server)
    is ListNotAvailable -> renderFailure(R.string.failure_movies_error
    }
    }

    View full-size slide

  78. Page heading
    Error Handling
    class MoviesViewModel
    @Inject constructor(private val store: MoviesStore) {
    var movies: MutableLiveData = MutableLiveData()
    var failure: MutableLiveData = MutableLiveData()
    fun loadMovies() =
    getMovies.reduce({ it.either(::handleFailure, ::handleMovieList) }, None())
    private fun handleMovieList(domainState: DomainState) {
    this.movies.value = MoviesViewState(domainState)
    }
    private fun handleFailure(failure: Failure) {
    this.failure.value = failure
    }
    }

    View full-size slide

  79. Page heading
    Error Handling
    class MoviesViewModel
    @Inject constructor(private val store: MoviesStore) {
    var movies: MutableLiveData = MutableLiveData()
    var failure: MutableLiveData = MutableLiveData()
    fun loadMovies() =
    getMovies.reduce({ it.either(::handleFailure, ::handleMovieList) }, None())
    private fun handleMovieList(domainState: DomainState) {
    this.movies.value = MoviesViewState(domainState)
    }
    private fun handleFailure(failure: Failure) {
    this.failure.value = failure
    }
    }

    View full-size slide

  80. Page heading
    Error Handling
    class MoviesViewModel
    @Inject constructor(private val store: MoviesStore) {
    var movies: MutableLiveData = MutableLiveData()
    var failure: MutableLiveData = MutableLiveData()
    fun loadMovies() =
    getMovies.reduce({ it.either(::handleFailure, ::handleMovieList) }, None())
    private fun handleMovieList(domainState: DomainState) {
    this.movies.value = MoviesViewState(domainState)
    }
    private fun handleFailure(failure: Failure) {
    this.failure.value = failure
    }
    }

    View full-size slide

  81. Page heading
    Error Handling
    class MoviesViewModel
    @Inject constructor(private val store: MoviesStore) {
    var movies: MutableLiveData = MutableLiveData()
    var failure: MutableLiveData = MutableLiveData()
    fun loadMovies() =
    getMovies.reduce({ it.either(::handleFailure, ::handleMovieList) }, None())
    private fun handleMovieList(domainState: DomainState) {
    this.movies.value = MoviesViewState(domainState)
    }
    private fun handleFailure(failure: Failure) {
    this.failure.value = failure
    }
    }

    View full-size slide

  82. Page heading
    Error Handling
    class MoviesViewModel
    @Inject constructor(private val store: MoviesStore) {
    var movies: MutableLiveData = MutableLiveData()
    var failure: MutableLiveData = MutableLiveData()
    fun loadMovies() =
    getMovies.reduce({ it.either(::handleFailure, ::handleMovieList) }, None())
    private fun handleMovieList(domainState: DomainState) {
    this.movies.value = MoviesViewState(domainState)
    }
    private fun handleFailure(failure: Failure) {
    this.failure.value = failure
    }
    }

    View full-size slide

  83. Page heading
    Error Handling
    class MoviesViewModel
    @Inject constructor(private val store: MoviesStore) {
    var movies: MutableLiveData = MutableLiveData()
    var failure: MutableLiveData = MutableLiveData()
    fun loadMovies() =
    getMovies.reduce({ it.either(::handleFailure, ::handleMovieList) }, None())
    private fun handleMovieList(domainState: DomainState) {
    this.movies.value = MoviesViewState(domainState)
    }
    private fun handleFailure(failure: Failure) {
    this.failure.value = failure
    }
    }

    View full-size slide

  84. Page heading
    Implementation
    Taking advantage of Kotlin

    View full-size slide

  85. Page heading
    The missing piece
    inline fun
    Activity.viewModel(factory: Factory, body: T.() -> Unit): T {
    val vm = ViewModelProviders.of(this, factory)[T::class.java]
    vm.body()
    return vm
    }

    View full-size slide

  86. Page heading
    The missing piece
    fun >
    LifecycleOwner.observe(liveData: L, body: (T?) -> Unit) =
    liveData.observe(this, Observer(body))
    fun >
    LifecycleOwner.failure(liveData: L, body: (Failure?) -> Unit) =
    liveData.observe(this, Observer(body))

    View full-size slide

  87. Page heading
    The missing piece
    fun >
    LifecycleOwner.observe(liveData: L, body: (T?) -> Unit) =
    liveData.observe(this, Observer(body))
    fun >
    LifecycleOwner.failure(liveData: L, body: (Failure?) -> Unit) =
    liveData.observe(this, Observer(body))

    View full-size slide

  88. Page heading
    The missing piece
    fun >
    LifecycleOwner.observe(liveData: L, body: (T?) -> Unit) =
    liveData.observe(this, Observer(body))
    fun >
    LifecycleOwner.failure(liveData: L, body: (Failure?) -> Unit) =
    liveData.observe(this, Observer(body))

    View full-size slide

  89. Page heading
    The missing piece
    //subscription to LiveData in MoviesViewModel
    moviesViewModel = viewModel(viewModelFactory) {
    observe(movies, ::renderMoviesList)
    failure(failure, ::handleFailure)
    }

    View full-size slide

  90. What’s next?

    View full-size slide

  91. Page heading
    The missing piece
    ViewState vs Event

    View full-size slide

  92. Page heading
    ViewState vs ViewEvent
    class BeaconViewModel(val store: BeaconStore) : ViewModel() {
    val viewState = MediatorLiveData()
    val viewEvents = MediatorLiveData()
    init {
    viewState.value = BeaconViewState.Idle
    viewState.addSource(reducer.subscribeToViewStates(), {
    if (viewState is BeaconViewState.Error) {
    viewState.shouldReThrow().let { throw viewState.exception }
    }
    viewState.setValue(it)
    })
    viewEvents.addSource(reducer.subscribeToEvents(), {
    viewEvents.setValue(it)
    })
    }
    fun interpret(action: BeaconAction) {
    reducer.reduce(action, viewState.value!!)
    }
    }

    View full-size slide

  93. Page heading
    ViewState vs ViewEvent
    class BeaconViewModel(val store: BeaconStore) : ViewModel() {
    val viewState = MediatorLiveData()
    val viewEvents = MediatorLiveData()
    init {
    viewState.value = BeaconViewState.Idle
    viewState.addSource(reducer.subscribeToViewStates(), {
    if (viewState is BeaconViewState.Error) {
    viewState.shouldReThrow().let { throw viewState.exception }
    }
    viewState.setValue(it)
    })
    viewEvents.addSource(reducer.subscribeToEvents(), {
    viewEvents.setValue(it)
    })
    }
    fun interpret(action: BeaconAction) {
    reducer.reduce(action, viewState.value!!)
    }
    }

    View full-size slide

  94. Page heading
    ViewState vs ViewEvent
    override fun reactTo(event: BeaconEvent) {
    when (event) {
    is BeaconEvent.SelectAttachment -> openFileSelector()
    is BeaconEvent.AttachmentDownloaded -> open(event.File)
    is BeaconEvent.Close -> closeActivity()
    }
    }

    View full-size slide

  95. What’s next?
    Kotlin Multiplatform
    DomainState persistence
    App as a State Machine

    View full-size slide

  96. Thank you!
    David González
    @dggonzalez
    [email protected]

    View full-size slide