Slide 1

Slide 1 text

A State Container based approach to Architecting Mobile applications David González @dggonzalez davidgonzalez@helpscout.com

Slide 2

Slide 2 text

Beacon Mobile SDK

Slide 3

Slide 3 text

Beacon Mobile SDK

Slide 4

Slide 4 text

Beacon Mobile SDK

Slide 5

Slide 5 text

Beacon Mobile SDK

Slide 6

Slide 6 text

Motivation

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Page heading Three traits of good design Balanced distribution of responsibilities

Slide 9

Slide 9 text

Page heading Three traits of good design Testability

Slide 10

Slide 10 text

Page heading Three traits of good design Easy to use

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Page heading Motivation Greenfield

Slide 13

Slide 13 text

Page heading Motivation Brownfield

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Beacon Mobile SDK

Slide 16

Slide 16 text

MVC

Slide 17

Slide 17 text

MVP

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

MVVM

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Page heading Motivation Null references The billion dollar mistake

Slide 22

Slide 22 text

A State Container approach

Slide 23

Slide 23 text

Page heading Implementation View State

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Page heading Implementation UI as a State Machine

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Page heading Implementation ViewModel

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Page heading Implementation ViewState vs DomainState

Slide 55

Slide 55 text

Page heading Implementation App = UI(state)

Slide 56

Slide 56 text

Page heading Implementation UI(viewState(domainState))

Slide 57

Slide 57 text

Page heading Implementation Store (Reducers)

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

Page heading The missing piece

Slide 66

Slide 66 text

Page heading The missing piece Functional Use Cases

Slide 67

Slide 67 text

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 }

Slide 68

Slide 68 text

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 }

Slide 69

Slide 69 text

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 }

Slide 70

Slide 70 text

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 }

Slide 71

Slide 71 text

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 }

Slide 72

Slide 72 text

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 }

Slide 73

Slide 73 text

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 }

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

Page heading The missing piece Error Handling

Slide 76

Slide 76 text

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 {...} }

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

Page heading Implementation Taking advantage of Kotlin

Slide 89

Slide 89 text

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 }

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

What’s next?

Slide 95

Slide 95 text

Page heading The missing piece ViewState vs Event

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

Thank you! David González @dggonzalez davidgonzalez@helpscout.com