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

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. A State Container based approach to Architecting Mobile applications David

    González @dggonzalez davidgonzalez@helpscout.com
  2. MVC

  3. MVP

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

    presenter = Presenter(viewController, model) viewController.presenter = presenter
  5. 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()
  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 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) }
  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 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) } }
  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 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 } } }
  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 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() }
  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 ViewModel abstract class BaseViewModel : ViewModel() { val

    state = MediatorLiveData<ViewState>() protected fun stateDidChange(viewState: ViewState){ state.value = viewState } abstract fun interpret(action: Action) }
  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 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) } }
  31. Page heading The missing piece interface Store { fun reduce(action:

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

    LoadingMovies : DomainState() data class MoviesLoaded(movies: List<Movies>) }
  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 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) } }
  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 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 }
  45. 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() } }
  46. 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> {...} }
  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 sealed class Failure { class

    NetworkConnection: Failure() class ServerError: Failure() /** Feature specific failures */ abstract class FeatureFailure: Failure() }
  51. 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 } }
  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 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 } }
  58. 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 }
  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 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))
  62. Page heading The missing piece //subscription to LiveData in MoviesViewModel

    moviesViewModel = viewModel(viewModelFactory) { observe(movies, ::renderMoviesList) failure(failure, ::handleFailure) }
  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 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!!) } }
  65. 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() } }