Model-View-Intent

Model-View-Intent architecture might seem very strange at first glance! By referring to Intent we don’t mean Android’s Intent, rather than a user’s interaction with our UI. Model is an object that processes information and keeps our state. View is the representation of the UI and cannot be rendered if Model is not present.

This architecture is heavily based on streams. If you use RxJava, every intent should be expressed as an Observable stream of events. For example, when a user clicks a button, you need to “translate” this to an Observable. Lucky for you, there is RxBinding that handles all of these translations.

MVI enforces unidirectional data flow. In a few words, unidirectional data flow provides predictable application state. This means, that a change on the UI layer will trigger an action on the data layer and the changes, that might occur, will transform the UI. This does not necessarily mean that the view directly affects storages etc. In particular View behaves as a pure function in which the same Model has the same output on the UI, without any side effects. As a result, when a bug occurs on the app, you can pinpoint it easier because data follows a “sequence” and the views are loosely coupled from the app data.

Ok, but how can I reproduce history?

Let’s say we have an app that does one thing: let’s you search GitHub users. The viewmodel for this simple app has 3 predefined states: Loading, Error and Success. Whenever user searches, we render the predictable state of Loading. If user found on GitHub, we render the Success state, otherwise something went wrong and we end up with Error state.

On the sample project, I use a cache that saves all these viewmodels and by using a slider, you can “time travel” over them.

So, we have the following View:

interface MainScreenView {

fun searchIntent(): Observable<String>

fun historyIntent(): Observable<Int>



fun render(viewModel: MainScreenViewModel)

fun renderFromHistory(viewModel: MainScreenViewModel)

}

Nothing difficult here. The searchIntent() is the Observable stream that gives us emissions of the search terms. The historyIntent() is the Observable stream from the Seekbar’s progress, which “travels” us back to previous/next states.

Our Model is the following:

data class MainScreenViewModel(

val searchTerm: String,

val showLoading: Boolean,

val showFields: Boolean,

val showError: Boolean,

val gitHubUser: GitHubUser? = null,

val errorMessage: String? = null

) {



val createdAt: Long = System.currentTimeMillis()



companion object {

fun inProgress(searchTerm: String): MainScreenViewModel = MainScreenViewModel(searchTerm, true, false, false) fun success(searchTerm: String, gitHubUser: GitHubUser) = MainScreenViewModel(searchTerm, false, true, false, gitHubUser) fun error(searchTerm: String): MainScreenViewModel = MainScreenViewModel(searchTerm, false, false, true, null,

"Houston we have a problem")

}

}

In our example, viewmodel is a data class because we want immutability! We actually don’t want someone else touching our state! The createdAt property exists in case we want to sort these viewmodels by time.

Finally, our Presenter is nothing special as well. I removed most of the lines, in order to focus on the most significant method: render(). Whenever the response is success, we map its result with the following factory method: success(it). Whenever the response is failure (mostly 404 Http status code- user not found), we use the error() factory method. Before that we emit an item that the request is in flight, using the inProgress() factory method. We save its state on dispatchRender(). The method doOnNext() is actually very helpful for side effects like logging, so we log our render events. Note: I could use the scan() operator, but I wanted to keep it simple.

class MainScreenPresenter {



...



fun bindIntents() {

val searchIntent = viewRef.get()?.searchIntent()?.share() ?: Observable.empty()



searchIntent

.debounce(400, MILLISECONDS, AndroidSchedulers.mainThread())

.doOnNext { Log.d("Intent", "Received Search Intent") }

.switchMap {

gitHubApi.githubDAO

.searchUser(it)

.map { MainScreenViewModel.success(it) }

.onErrorReturn { MainScreenViewModel.error() }

.startWith(MainScreenViewModel.inProgress())

}

.doOnNext { Log.d("Render", it.toString()) }

.observeOn(AndroidSchedulers.mainThread())

.subscribeWith(object : DisposableObserver<MainScreenViewModel>() { override fun onNext(viewModel: MainScreenViewModel) {

dispatchRender(viewModel)

}



override fun onError(e: Throwable) {

Log.wtf("ragment", e.message)

}



override fun onComplete() {

}



})

.addTo(compositeDisposable)

} private fun dispatchRender(viewModel: MainScreenViewModel) {

ViewModelCache.append(viewModel)

viewRef.get()?.render(viewModel)

} ... }

The View’s implementation is like this:

progressbar.showOrHideInvisible(viewModel.showLoading)

userName.showOrHideInvisible(viewModel.showFields)

userLogin.showOrHideInvisible(viewModel.showFields)

gitHubUrl.showOrHideInvisible(viewModel.showFields)



userName.text = viewModel.gitHubUser?.name

userLogin.text = viewModel.gitHubUser?.login

gitHubUrl.text = viewModel.gitHubUser?.url



if (viewModel.showError) {

Toast.makeText(this, viewModel.errorMessage, Toast.LENGTH_LONG).show()

}

Actually this is the only pain-point of this pattern. You somehow need to smartly and effectively invalidate Android’s views without worrying about how many layout passes our ViewGroup has executed. In an upcoming Medium post we will discuss on how to fix these issues.

Actually that’s all! You see? Nothing too hard. The result of the above looks like this: