Basic MVVM in Android

What is MVVM?

MVVM stand for View - ViewModel - Model and is a way to divide the software into parts.

Diagram show the relationship between View, ViewModel and Model.

As you can see in the image above, the View knows the ViewModel interface. But the ViewModel knows nothing about the View. The same is true for the ViewModel, it knows about the Model interface. But the Model knows nothing about the ViewModel.

Changing the View do not affect the ViewModel or the Model. The same is true with the ViewModel, it’ll not affect the Model and the Model will not affect the View.

This article only writes about the View and the ViewModel. Another article will write about the Model.

Add the MVVM library to the project.

dependencies {
    /* … */
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    /* … */
}

View in Android

A View in an Android App is an Activity or a Fragment. The only thing the View should handle is the user interface. The View should not contain any logic, it’s handle in the ViewModel instead.

The widget data and events is bind to ViewModel. If the data is changeable, the View observe changes and update the user interface from it.

In Android, LiveData handle changeable data. A LiveData knows about the state of Activity or Fragment and can stop updating the View if it’s not active.

Binding

A provider class gets the reference to the ViewModel in the View.

// Get the reference to the ViewModel.
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

There are four ways to bind a widgets to a ViewModel: static, changeable, callback and two-way.

Static Data Binding

Find the widget and set the value.

// Example of binding static data.
bindStaticData(R.id.staticData, viewModel.staticData)

/* ... */

private fun bindStaticData(viewId: Int, staticData: String) {
    findViewById<TextView>(viewId).text = staticData
}

Changeable Data

Observe the data and update the widget when it’s changed.

// Example of binding changeable data.
bindChangeableData(R.id.changeableData, viewModel.changeableData)

/* ... */

private fun bindChangeableData(viewId: Int, changeableData: LiveData<String>) {
    val textView = findViewById<TextView>(viewId)
    changeableData.observe(this, Observer<String> { textView.text = it })
}

Callback Listening

Add a listener to the widget and call a ViewModel method when an event has occurs.

// Example of update changeable data.
bindClickUpdateData(R.id.leftButton, "Left button clicked!", viewModel::onUpdateChangeableData)
bindClickUpdateData(R.id.rightButton, "Right button clicked!", viewModel::onUpdateChangeableData)

/* ... */

private fun bindClickUpdateData(viewId: Int, newData: String, callback: (String) -> Unit) {
    findViewById<Button>(viewId).setOnClickListener { callback(newData) }
}

Two-Way Data Binding

Two-way binding observer changes of the data and listening for event from the widget. The data must be check so it’s not already is set. Otherwise, there is a risk for an infinitive loop.

// Example of two-way binding changeable data.
bindTwoWay(R.id.twoWayBinding, viewModel.changeableData)

/* ... */

private fun bindTwoWay(viewId: Int, changeableData: MutableLiveData<String>) {
    val editText = findViewById<EditText>(viewId)

    // Observe changes to the changeable data and update the input text.
    changeableData.observe(this, Observer<CharSequence> {
        // It's imported to check that the text has changed to avoid infinitive loop.
        if(haveContentsChanged(it, editText.text)) {
            editText.setText(it)
        }
    })

    // Check if the text has changed and update the changeable data if it has
    editText.addTextChangedListener(object: TextWatcher {
        override fun afterTextChanged(s: Editable?) {}
        override fun beforeTextChanged(text: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(text: CharSequence?, start: Int, before: Int, count: Int) {
            changeableData.value = text.toString()
        }
    })
}

ViewModel

A ViewModel is as the name suggest, the Model for the View. The role of the ViewModel is to prepare and handle data from the Model and to handle logic for the View.

The ViewModel is a singleton. Fragments can share data with it common Activity parent ModelView.

It survives configuration changes, like turning the phone. But it will not survive destruction of the View.

It’s imported not to reference to an Activity or a Fragment from the ViewModel. If there is a reference to an Activity or Fragment and it’s destroyed during a configuration change. The garbage collector will not be able remove the old Activity or Fragment and a memory lost will occur.

But, how is system data, like resources accessed from the ViewModel. You may ask?

You can have a reference to the Application in the ViewModel, but it’s not recommended! This will make it hard to write unit test for the ViewModel. It’s better to get this data from a Model, so it can be faked during unit testing.

Some things needs a references to the Activity. This has to be done within a View or a Model and will breaks the rule of no logic in the View. If you need the reference in a Model, send it as an argument in the OnEvent… function call.

An example of this is to start and Intent or finish the currect Actvity.

Convert Changeable Data

A changeable data type is convertible with the Transformations class. Here is an example that convert a name of a color to a color integer.

val colorAsString = MutableLiveData<String>().apply { value = "Red" }

val colorAsHex = Transformations.map(colorAsString) { color ->
    when(color.toLowerCase(Locale.getDefault())) {
        "red"     -> 0xFFFF0000
        "blue"    -> 0xFF0000FF
        "green"   -> 0xFF00FF00
        "yellow"  -> 0xFFFFFF00
        "magenta" -> 0xFFFF00FF
        "cyan"    -> 0xFF00FFFF
        "white"   -> 0xFFFFFFFF
        "black"   -> 0xFF000000
        else      -> 0xFF888888
    }
}

Merge Changeable Data Sources

Changeable data from many sources is mergeable into one with the Mediator class. Here is an example of adding two numbers to the sum of total.

val augend = MutableLiveData<String>().apply { value = "1" }

val addend = MutableLiveData<String>().apply { value = "1" }

val sumOfTotal = MediatorLiveData<String>().apply {
    addSource(augend) {
        val a = if(it.isNotEmpty()) it.toInt() else 0
        val b = if(addend.value != null && addend.value!!.isNotEmpty()) addend.value!!.toInt() else 0
        value = (a + b).toString()
    }
    addSource(addend) {
        val a = if(augend.value != null && addend.value!!.isNotEmpty()) augend.value!!.toInt() else 0
        val b = if(it.isNotEmpty()) it.toInt() else 0
        value = (a + b).toString()
    }
}

Summary

In this article we learn the basic of how MVVM works in Android. How to create ViewModel and get a reference to it from the View.

Bind Widget to the ViewModel and converting changable data was also shown.

You can find the source files on GitHub!

Read the next post: Model Injection using MVVM