4. MVC
PROBLEMS
▸ Not reactive in nature
▸ A lot of developers don’t agree which is the View and which is the Controller
▸ Ending up with God Controllers was easy
▸ Hard to test
10. MVI?
▸ Model View Intent architecture is a pattern that is reactive, event based,
immutable and managed as a state machine.
▸ Model: Is the current state of your view
▸ View: is the view!
▸ Intent: Events that eventually reduces our Model to new Models or States
12. MVI
WHY?
▸ Reactive
▸ Functional
▸ Unidirectional Data Flow
▸ Immutable States
▸ Single Source of Truth
▸ Acts as documentation
▸ Easy to test and debug (Time Travel bonus)
▸ Better integration with designers
▸ Kotlin friendly
14. MVI
HOW? 5 STEPS
▸ Define an initial state
▸ Events from the User are mapped to actions -> Sealed Classes
▸ Actions are mapped to Results -> Sealed Classes
▸ Results + Old State = New State -> State Machine
▸ Bind new state to view -> Data-binding
16. SHOW ME THE CODE
DATA CLASSES
▸ BaseEvent<T>
▸ Result<S>
▸ UIModel<S>
17. SHOW ME THE CODE
BASEEVENT<T>
interface BaseEvent<T> {
fun getPayLoad(): T
}
18. SHOW ME THE CODE
BASEEVENT<T>
interface BaseEvent<T> {
fun getPayLoad(): T
}
RESULT<S>
sealed class Result<S> {
abstract val event: BaseEvent<*>
}
data class LoadingResult(override val event: BaseEvent<*>) : Result<Nothing>()
data class ErrorResult(val error: Throwable,
override val event: BaseEvent<*>) : Result<Nothing>()
data class SuccessResult<S>(val bundle: S,
override val event: BaseEvent<*>) : Result<S>()
19. SHOW ME THE CODE
UIMODEL<S>
sealed class UIModel<S> {
abstract val event: BaseEvent<*>
abstract val bundle: S
override fun toString() = "stateEvent: $event"
}
data class LoadingState<S>(override val bundle: S,
override val event: BaseEvent<*>) : UIModel<S>() {
override fun toString() = "State: Loading, " + super.toString()
}
data class ErrorState<S>(val error: Throwable,
val errorMessage: String,
override val bundle: S,
override val event: BaseEvent<*>) : UIModel<S>() {
override fun toString() = "State: Error, Throwable: $error, " + super.toString()
}
data class SuccessState<S>(override val bundle: S,
override val event: BaseEvent<*> = EmptyEvent) : UIModel<S>() {
override fun toString() = "State: Success, Bundle: $bundle, " + super.toString()
}
20. SHOW ME THE CODE
REPRESENTING STATE IN UIMODEL<S> ??
sealed class ListState {
abstract val list: List<ItemInfo>
abstract val page: Int
abstract val callback: DiffUtil.DiffResult
}
@Parcelize
data class EmptyState(override val list: List<ItemInfo> = emptyList(),
override val page: Int = 0,
override val callback: DiffUtil.DiffResult) : ListState(), Parcelable
@Parcelize
data class GetState(override val list: List<ItemInfo> = emptyList(),
override val page: Int = 1,
override val callback: DiffUtil.DiffResult) : ListState(), Parcelable
21. SHOW ME THE CODE
VIEWMODEL
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>>
22. SHOW ME THE CODE
VIEWMODEL - SWITCH TO A BACKGROUND THREAD
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>> {
events.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.computation())
}
23. SHOW ME THE CODE
VIEWMODEL - MAP EVENTS TO ACTIONS
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>> {
events.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.computation())
.concatMap {
Flowable.just(it)
.concatMap(mapEventsToActions())
}
}
fun mapEventsToActions(): Function<BaseEvent<*>, Flowable<*>> {
return Function { event ->
val userListEvent = event as UserListEvents
when (userListEvent) {
is GetPaginatedUsersEvent -> getUsers(userListEvent.getPayLoad())
is DeleteUsersEvent -> deleteCollection(userListEvent.getPayLoad())
is SearchUsersEvent -> search(userListEvent.getPayLoad())
}
}
}
24. SHOW ME THE CODE
VIEWMODEL - MAP ACTIONS RESPONSES TO RESULTS
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>> {
events.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.computation())
.concatMap {
Flowable.just(it)
.concatMap(mapEventsToActions())
.map<Result<*>> { SuccessResult(it) }
.onErrorReturn { ErrorResult(it) }
.startWith(LoadingResult())
}
}
25. SHOW ME THE CODE
VIEWMODEL - STATE MACHINE
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>> {
events.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.computation())
.concatMap {
Flowable.just(it)
.concatMap(mapEventsToActions())
.map<Result<*>> { SuccessResult(it) }
.onErrorReturn { ErrorResult(it) }
.startWith(LoadingResult())
}
.scan<UIModel<S>>(SuccessState(initialState), reducer())
}
26. SHOW ME THE CODE
VIEWMODEL - REDUCER
fun reducer(): BiFunction<UIModel<S>, Result<*>, UIModel<S>> =
BiFunction { currentUIModel, result ->
result.run {
when (this) {
is LoadingResult -> when (currentUIModel) {
is LoadingState ->
throw IllegalStateException(getErrorMessage(currentUIModel, this, LOADING_STATE))
is SuccessState -> LoadingState(currentUIModel.bundle, event)
is ErrorState -> LoadingState(currentUIModel.bundle, event)
}
is ErrorResult -> when (currentUIModel) {
is LoadingState -> ErrorState(currentUIModel.bundle, error, event)
is SuccessState ->
throw IllegalStateException(getErrorMessage(currentUIModel, this, SUCCESS_STATE))
is ErrorState ->
throw IllegalStateException(getErrorMessage(currentUIModel, this, ERROR_STATE))
}
is SuccessResult<*> -> when (currentUIModel) {
is SuccessState ->
SuccessState(stateReducer()
.invoke(bundle!!, event, currentUIModel.bundle), event)
is LoadingState -> SuccessState(stateReducer()
.invoke(bundle!!, event, currentUIModel.bundle), event)
is ErrorState ->
throw IllegalStateException(getErrorMessage(currentUIModel, this, ERROR_STATE))
}
}
}
}
27. SHOW ME THE CODE
VIEWMODEL - SUCCESS STATE REDUCER
fun stateReducer(): (newResult: Any, event: BaseEvent<*>, currentStateBundle: UserListState) -> UserListState {
return { newResult, _, currentStateBundle ->
when (currentStateBundle) {
is EmptyState -> when (newResult) {
is List<*> -> {
// Calculate your new State
GetState(pair.first, currentStateBundle.lastId, pair.second)
}
else -> throw IllegalStateException("Can not reduce EmptyState with this result: $newResult!")
}
is GetState -> when (newResult) {
is List<*> -> {
// Calculate your new State
GetState(pair.first, currentStateBundle.lastId + 1, pair.second)
}
else -> throw IllegalStateException("Can not reduce GetState with this result: $newResult!")
}
}
}
}
28. SHOW ME THE CODE
VIEWMODEL - SWITCH BACK TO MAIN THREAD
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>> {
events.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.computation())
.concatMap {
Flowable.just(it)
.concatMap(mapEventsToActions())
.map<Result<*>> { SuccessResult(it) }
.onErrorReturn { ErrorResult(it) }
.startWith(LoadingResult())
}
.scan<UIModel<S>>(SuccessState(initialState), reducer())
.observeOn(AndroidSchedulers.mainThread())
}
29. SHOW ME THE CODE
VIEWMODEL - CACHE LAST EMITTED STATE
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>> {
events.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.computation())
.concatMap {
Flowable.just(it)
.concatMap(mapEventsToActions())
.map<Result<*>> { SuccessResult(it) }
.onErrorReturn { ErrorResult(it) }
.startWith(LoadingResult())
}
.scan<UIModel<S>>(SuccessState(initialState), reducer())
.observeOn(AndroidSchedulers.mainThread())
.replay(1)
.autoConnect()
}
30. SHOW ME THE CODE
VIEWMODEL - CACHE LAST EMITTED STATE
fun store(events: Observable<BaseEvent<*>>, initialState: S): Flowable<UIModel<S>> {
events.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(Schedulers.computation())
.concatMap {
Flowable.just(it)
.concatMap(mapEventsToActions())
.map<Result<*>> { SuccessResult(it) }
.onErrorReturn { ErrorResult(it) }
.startWith(LoadingResult())
}
.scan<UIModel<S>>(SuccessState(initialState), reducer())
.observeOn(AndroidSchedulers.mainThread())
.compose(ReplayingShare.instance())
}
33. SHOW ME THE CODE
VIEW
override fun onStart() {
super.onStart()
viewModel.store(events(), initialState()).toLiveData()
.observe(this, Observer { uiModel: UIModel<S>? ->
uiModel?.apply {
view.toggleViews(this is LoadingState, event)
when (this) {
is ErrorState -> showError(errorMessage, event)
is SuccessState -> {
setState(bundle)
renderSuccessState(bundle)
}
}
}
})
}
34. SHOW ME THE CODE
BINDING SUCCESS STATE
override fun renderSuccessState(successState: UserListState) {
when (successState) {
is EmptyState -> TODO("Provide your binding here")
is GetState -> TODO("Provide your binding here")
}
}
36. MVI
TESTING
▸ Unit testing:
▸ Test your Actions
▸ Test your Reductions
▸ E2E testing:
▸ Mock your Actions
▸ Fire Events in events stream using a Subject
▸ Assert that the correct states are the coming out
▸ UI Testing:
▸ Screenshot comparison
38. PROS & CONS
PROS
▸ Reactive
▸ Functional
▸ Predictable
▸ Unidirectional Data Flow
▸ Immutable States
▸ Single Source of Truth
▸ Acts as documentation
▸ Easy to test and debug
▸ Kotlin friendly
39. PROS & CONS
CONS
▸ Boilerplate
▸ Change of mindset / Steep learning curve
▸ Creation & Destruction of a lot object instances.
▸ States need to be static. No Toast messages!