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 Slide

  2. Beacon Mobile SDK

    View Slide

  3. Beacon Mobile SDK

    View Slide

  4. Beacon Mobile SDK

    View Slide

  5. Beacon Mobile SDK

    View Slide

  6. Motivation

    View Slide

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

    View Slide

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

    View Slide

  9. Page heading
    Three traits of good design
    Testability

    View Slide

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

    View Slide

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

    View Slide

  12. Page heading
    Motivation
    Greenfield

    View Slide

  13. Page heading
    Motivation
    Brownfield

    View Slide

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

    View Slide

  15. Beacon Mobile SDK

    View Slide

  16. MVC

    View Slide

  17. MVP

    View Slide

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

    View Slide

  19. MVVM

    View Slide

  20. 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 Slide

  21. Page heading
    Motivation
    Null references
    The billion dollar mistake

    View Slide

  22. A State Container
    approach

    View Slide

  23. Page heading
    Implementation
    View State

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

  25. 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 Slide

  26. 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 Slide

  27. 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 Slide

  28. 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 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 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 Slide

  31. 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 Slide

  32. 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 Slide

  33. 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 Slide

  34. 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 Slide

  35. Page heading
    Implementation
    UI as a State
    Machine

    View Slide

  36. 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 Slide

  37. 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 Slide

  38. 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 Slide

  39. 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 Slide

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

    View Slide

  41. 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 Slide

  42. 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 Slide

  43. 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 Slide

  44. Page heading
    Implementation
    ViewModel

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    }
    }

    View Slide

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

    View Slide

  51. 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 Slide

  52. 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 Slide

  53. 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 Slide

  54. Page heading
    Implementation
    ViewState vs DomainState

    View Slide

  55. Page heading
    Implementation
    App = UI(state)

    View Slide

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

    View Slide

  57. Page heading
    Implementation
    Store (Reducers)

    View Slide

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

    View Slide

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

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

  61. 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 Slide

  62. 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 Slide

  63. 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 Slide

  64. 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 Slide

  65. Page heading
    The missing piece

    View Slide

  66. Page heading
    The missing piece
    Functional Use Cases

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

  70. 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 Slide

  71. 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 Slide

  72. 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 Slide

  73. 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 Slide

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

    View Slide

  75. Page heading
    The missing piece
    Error Handling

    View Slide

  76. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  81. 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 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 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 Slide

  84. 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 Slide

  85. 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 Slide

  86. 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 Slide

  87. 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 Slide

  88. Page heading
    Implementation
    Taking advantage of Kotlin

    View Slide

  89. 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 Slide

  90. 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 Slide

  91. 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 Slide

  92. 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 Slide

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

    View Slide

  94. What’s next?

    View Slide

  95. Page heading
    The missing piece
    ViewState vs Event

    View Slide

  96. 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 Slide

  97. 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 Slide

  98. 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 Slide

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

    View Slide

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

    View Slide