Share
Embed
Transcript
100 slides
-
Page heading Motivation Explore a common (and shared) architecture language
(and code) between iOS and Android -
Page heading Three traits of good design Balanced distribution of
responsibilities -
Good code is the one that you can easily delete
- David González -
A good design is easier to change than a bad
design - Dave Thomas -
Page heading The assembly problem let viewController = UIViewController() let
presenter = Presenter(viewController, model) viewController.presenter = presenter -
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() -
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) } -
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) } -
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) } -
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) } -
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) } -
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) } } -
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) } } -
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) } } -
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) } } -
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) } } -
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) } } -
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 } } } -
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 } } } -
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 } } } -
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 } } } -
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() } -
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() } -
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() } -
Page heading ViewModel abstract class BaseViewModel : ViewModel() { val
state = MediatorLiveData<ViewState>() protected fun stateDidChange(viewState: ViewState){ state.value = viewState } abstract fun interpret(action: Action) } -
Page heading ViewModel abstract class BaseViewModel : ViewModel() { val
state = MediatorLiveData<ViewState>() protected fun stateDidChange(viewState: ViewState){ state.value = viewState } abstract fun interpret(action: Action) } -
Page heading ViewModel abstract class BaseViewModel : ViewModel() { val
state = MediatorLiveData<ViewState>() protected fun stateDidChange(viewState: ViewState){ state.value = viewState } abstract fun interpret(action: Action) } -
Page heading ViewModel abstract class BaseViewModel : ViewModel() { val
state = MediatorLiveData<ViewState>() protected fun stateDidChange(viewState: ViewState){ state.value = viewState } abstract fun interpret(action: Action) } -
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) } } -
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) } } -
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) } } -
Page heading The missing piece interface Store { fun reduce(action:
Action) fun subscribe() LiveData<DomainState> } -
Page heading The missing piece sealed class DomainState { object
LoadingMovies : DomainState() data class MoviesLoaded(movies: List<Movies>) } -
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) } } -
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) } } -
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) } } -
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) } } -
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) } } -
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 } -
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 } -
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 } -
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 } -
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 } -
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 } -
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 } -
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() } } -
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> {...} } -
Page heading The missing piece sealed class Failure { class
NetworkConnection: Failure() class ServerError: Failure() /** Feature specific failures */ abstract class FeatureFailure: Failure() } -
Page heading The missing piece sealed class Failure { class
NetworkConnection: Failure() class ServerError: Failure() /** Feature specific failures */ abstract class FeatureFailure: Failure() } -
Page heading The missing piece sealed class Failure { class
NetworkConnection: Failure() class ServerError: Failure() /** Feature specific failures */ abstract class FeatureFailure: Failure() } -
Page heading The missing piece sealed class Failure { class
NetworkConnection: Failure() class ServerError: Failure() /** Feature specific failures */ abstract class FeatureFailure: Failure() } -
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 } } -
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 } } -
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 } } -
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 } } -
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 } } -
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 } } -
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 } } -
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 } -
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)) -
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)) -
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)) -
Page heading The missing piece //subscription to LiveData in MoviesViewModel
moviesViewModel = viewModel(viewModelFactory) { observe(movies, ::renderMoviesList) failure(failure, ::handleFailure) } -
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!!) } } -
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!!) } } -
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() } }