Skip to content

Android Single Source of Truth (SSOT) using View States with MVVM and LiveData

The Single Source of Truth (SSOT) principle is an important concept not only in Android, but in pretty much every type of object-oriented programming.

The idea of it in an MVVM sense is that the view only receives data from one observable (be it LiveData or RxJava).

As important as this concept is in OOT programming, I don’t hear it being discussed enough in the Android community.

How I will be doing it here is a set of classes I personally call View States. They hold data for each field in the view is supposed to display, such as the contents of a TextView (String) or whether or not a button is enabled (Boolean).

Prerequisites

This tutorial relies on you having an understanding of MVVM and LiveData.

The View State

open class ViewState (
    var newActivity: KClass<*>? = null,
    var clearActivityOnIntent: Boolean = false,
    var firebaseUser: FirebaseUser? = null
)

This is the base View State class I’m using for my MVVM Firestore Chat Application (which I’ll be covering soon). This is of course, a base class which I’ll have other view states inherit from (e.g. LoginViewState).

A common problem in MVVM is that the ViewModel shouldn’t include any Android code, which makes the simple task of handling a click to start an Intent suddenly very daunting.

The newActivity field here is one possible solution to this problem. If the View detects that this isn’t null, it will launch that activity. You’ll see it further down.

class LoginViewState (
    var submitEnabled: Boolean = true,
    var errorMessage: String = "",
    firebaseUser: FirebaseUser? = null,
    newActivity: KClass<*>? = null,
    clearActivityOnIntent: Boolean = false
): ViewState(
    newActivity,
    clearActivityOnIntent
)

I’ll be using this LoginViewState for my examples down below. It has fields more directly connected to the views, such as submitEnabled which represents a button’s enabled status.

Inside the ViewModel

class LoginViewModel @Inject constructor(
    val firebaseRepository: FirebaseRepository,
    viewState: LoginViewState
): BaseViewModel<LoginViewState>(viewState) {

...

}

I have the ViewState injected into the ViewModel’s constructor using Dagger.

val errorHandler = CoroutineExceptionHandler { _, exception ->
    exception.printStackTrace()
    viewState.errorMessage = "Please enter the correct email and password"
    viewState.submitEnabled = true
    updateUi()
}

networkScope.launch(errorHandler) {
    firebaseRepository.login(email, password)
    viewState.newActivity = LobbyActivity::class
    viewState.clearActivityOnIntent = true
    updateUi()
}
fun updateUi() {
    stateLiveData.postValue(viewState)
}

Two such examples where I’m editing the view state’s values. After setting the values the way I want them, I call updateUI() which posts the LiveData that contains the state.

Inside the View

protected fun observeState() {
    viewModel.getState().observe(this, Observer { state -> updateUi(state as S) })
}


abstract fun updateUi(state: S)

In my BaseView, I have a simple observer for the ViewState LiveData, which calls an abstract updateUI(state) function, which is then handled by the specific fragment.

You can also see that I’m using a generic type S so that I can implement this function using different ViewState classes. If you would like me to go over how I did this, leave it in the comments.

override fun updateUi(state: LoginViewState) {

    button_login.isEnabled = state.submitEnabled
    state.newActivity?.start(state.clearActivityOnIntent)
    checkUserLoggedIn(state.firebaseUser)
    textview_error_login.text = state.errorMessage
}

One such implementation of updateUI(state) is this in my LoginFragment. The state’s fields are applied to the UI in simple easy to read one-liners.

Thanks to the beauty of Kotlin, even checking the NewActivity field is a simple one-liner using the ? operator. If you’re sharp, you may have noticed that neither the Activity or KClass classes have a start function.

protected fun KClass<*>.start(clearingLast: Boolean) {
    val intent = Intent(activity, this.java)
    startActivity(intent)
    viewModel.resetNewActivity()
    if (clearingLast) activity?.finish()
}

I may be going on a tangent here, but this is one such place where Kotlin’s extension functions really shine. This function allows me to call start(clearingLast) on any KClass<*> object I’m using in my state field for newActivity.

Source Code

I don’t plan to release the source code for this just yet, but instead I will be releasing this as a complete chat application implementing MVVM, LiveData, Coroutines, and Dagger as my MVVM FirestoreChatApplication.

I’m hoping I can get that done next week. Stay tuned, and happy coding.