Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

A State Container Architecture for mobile appli...

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

  2. MVP

  3. Page heading The assembly problem let viewController = UIViewController() let

    presenter = Presenter(viewController, model) viewController.presenter = presenter
  4. 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()
  5. Page heading ViewState sealed class ReplyViewState : ViewState() { data

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

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

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

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

    class Form(val formOptions: BeaconContactForm, val message: String, val attachments: Map<String, AttachmentsViewState>, val formValid: Boolean, val draft: String) : ReplyViewState() object SendingReply : ReplyViewState() object ReplySent : ReplyViewState() class SendReplyError(error: Throwable) : ViewState.Error(error) }
  10. 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) } }
  11. 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) } }
  12. 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) } }
  13. 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) } }
  14. 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) } }
  15. 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) } }
  16. 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 } } }
  17. 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 } } }
  18. 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 } } }
  19. 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 } } }
  20. 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() }
  21. 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() }
  22. 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() }
  23. Page heading ViewModel abstract class BaseViewModel : ViewModel() { val

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

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

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

    state = MediatorLiveData<ViewState>() protected fun stateDidChange(viewState: ViewState){ state.value = viewState } abstract fun interpret(action: Action) }
  27. Page heading Implementation viewModel.viewState.observe(this, Observer<ViewState> { 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) } }
  28. Page heading Implementation viewModel.viewState.observe(this, Observer<ViewState> { 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) } }
  29. Page heading Implementation viewModel.viewState.observe(this, Observer<ViewState> { 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) } }
  30. Page heading The missing piece interface Store { fun reduce(action:

    Action) fun subscribe() LiveData<DomainState> }
  31. Page heading The missing piece sealed class DomainState { object

    LoadingMovies : DomainState() data class MoviesLoaded(movies: List<Movies>) }
  32. 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) } }
  33. 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) } }
  34. 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) } }
  35. 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) } }
  36. 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) } }
  37. Page heading The missing piece abstract class UseCase<out Type, in

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

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

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

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

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

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

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

    : UseCase<DomainState, UseCase.None>() { override suspend fun run(params: None): Either<Failure, DomainState> { return repository.getAllMovies() } }
  45. Page heading The missing piece sealed class Either<out L, out

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

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

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

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

    NetworkConnection: Failure() class ServerError: Failure() /** Feature specific failures */ abstract class FeatureFailure: Failure() }
  50. 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 } }
  51. Page heading Error Handling class MoviesViewModel @Inject constructor(private val store:

    MoviesStore) { var movies: MutableLiveData<MoviesViewState> = MutableLiveData() var failure: MutableLiveData<Failure> = 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 } }
  52. Page heading Error Handling class MoviesViewModel @Inject constructor(private val store:

    MoviesStore) { var movies: MutableLiveData<MoviesViewState> = MutableLiveData() var failure: MutableLiveData<Failure> = 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 } }
  53. Page heading Error Handling class MoviesViewModel @Inject constructor(private val store:

    MoviesStore) { var movies: MutableLiveData<MoviesViewState> = MutableLiveData() var failure: MutableLiveData<Failure> = 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 } }
  54. Page heading Error Handling class MoviesViewModel @Inject constructor(private val store:

    MoviesStore) { var movies: MutableLiveData<MoviesViewState> = MutableLiveData() var failure: MutableLiveData<Failure> = 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 } }
  55. Page heading Error Handling class MoviesViewModel @Inject constructor(private val store:

    MoviesStore) { var movies: MutableLiveData<MoviesViewState> = MutableLiveData() var failure: MutableLiveData<Failure> = 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 } }
  56. Page heading Error Handling class MoviesViewModel @Inject constructor(private val store:

    MoviesStore) { var movies: MutableLiveData<MoviesViewState> = MutableLiveData() var failure: MutableLiveData<Failure> = 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 } }
  57. Page heading The missing piece inline fun <reified T :

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

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

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

    : LiveData<T>> LifecycleOwner.observe(liveData: L, body: (T?) -> Unit) = liveData.observe(this, Observer(body)) fun <L : LiveData<Failure>> LifecycleOwner.failure(liveData: L, body: (Failure?) -> Unit) = liveData.observe(this, Observer(body))
  61. Page heading The missing piece //subscription to LiveData in MoviesViewModel

    moviesViewModel = viewModel(viewModelFactory) { observe(movies, ::renderMoviesList) failure(failure, ::handleFailure) }
  62. Page heading ViewState vs ViewEvent class BeaconViewModel(val store: BeaconStore) :

    ViewModel() { val viewState = MediatorLiveData<BeaconViewState>() val viewEvents = MediatorLiveData<BeaconEvent>() 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!!) } }
  63. Page heading ViewState vs ViewEvent class BeaconViewModel(val store: BeaconStore) :

    ViewModel() { val viewState = MediatorLiveData<BeaconViewState>() val viewEvents = MediatorLiveData<BeaconEvent>() 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!!) } }
  64. 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() } }