MVVM and DataBinding: Android Design Patterns

This article describes the MVVM Design Pattern and its components, data binding, and other design patterns and architectural concepts for the Android platform. By Matei Suica.

4.5 (47) · 1 Review

Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Defining the Role of the View

In MVVM, the View is very lightweight because it has almost no logic in it. The View is everything UI related. In Android, this is usually an Activity or a Fragment. Its role is to display whatever it receives from the ViewModel and forward input to it. It’s as simple as that!

Here’s an example View in an Android class named MainFragment:

class MainFragment : Fragment() {

  companion object {
    fun newInstance() = MainFragment()
  }

  private var selectedUser: User? = null
  private lateinit var viewModelBasic: BasicMainViewModel

  override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    return inflater.inflate(R.layout.main_fragment, container, false)
  }

  override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    viewModelBasic = BasicMainViewModel()
    viewModelBasic.uiModel.observe(this, Observer<UiModel>{ uiModel ->
      // update UI
    })
  }

  fun onDeleteButtonClicked(v: View) {
    viewModelBasic.deleteUser(selectedUser)
  }
}

In onActivityCreated(), a ViewModel object is instantiated from the class BasicMainViewModel. In the onDeleteButtonClicked() handler, you see a typical interaction between View and ViewModel: The fragment passes a UI event, the user clicking the delete button, off to the ViewModel.

Defining the Model

The Model is a lightweight component that doesn’t know about any other component. Its role is to store the state of the system and let the ViewModel query it.

The Model is often created using a Repository pattern. Data Access Objects (DAO) that provide Create-Read-Update-Delete (CRUD) operations can help the repository fulfill requests. For Android, the Google team released the Room Persistence library within Jetpack for this purpose.

A simple Model interface could look like this:

interface Repository {

  fun insert(user: User)

  fun getAll(): LiveData<List<User>>

  fun delete(user: User)

  fun update(user: User)
}

The interface provides Create (insert()), Read (getAll()), Update (update()) and Delete (delete()) operations on th Model.

You will see an example implementation with Room later in the article.

Finding the ViewModel’s Place in MVVM

The ViewModel is the centerpiece of MVVM. It has all the business logic and some of the display logic in it.

The ViewModel needs to adapt the information in the Model for the Views to display. The ViewModel also transforms input events into data for the Model to store. If something goes wrong, look into the ViewModel first!

Here’s what BasicMainViewModel might look like:

class BasicMainViewModel(private val repository: Repository) : ViewModel() {
  val uiModel: MutableLiveData<UiModel> by lazy {
    MutableLiveData< UiModel >()
  }

  fun generateReport (){
    // TODO Add complicated report computation
  }

  fun deleteUser(user: User?) {
    deleteUserFromRepository(user)
    uiModel.value?.userList?.remove(user)
  }

  fun deleteUserFromRepository(user: User?) {
    if(user != null) { repository.delete(user) }
  }
}

The ViewModel here takes a Repository in its constructor so that it can communicate with the Model. And it provides functions that calculate data for display (generateReport()) and allow deletion of Model data.

Improving Testability

Writing testable code is one trait of a good developer. After reading this article, that’ll be you. :]

Hiding Behind Interfaces and Dependency Injection

The easiest thing you can do with your code to improve testability is to hide it behind an interface. This means that your class will implement an interface that exposes only the public methods. The public methods form a type of Application Programming Interface (API).

You’ll need to think twice about what you expose for your API. Once something is in the API, other parts of the code will use it. The API is like a contract that methods can’t change. The advantage is that you can change the implementation without breaking anything.

For example the class BasicMainViewModel should hide behind an interface like this:

interface MainViewModel {
  fun generateReport ()
  fun deleteUser(user: User?)
  fun deleteUserFromRepository(user: User?)
}

Note: Have you noticed the Repository in the constructor? That’s an interface too. This is an example of “dependency injection”: Allowing other objects to “inject” a Repository in the ViewModel.

You’ll need to create interfaces for all components of the MVVM, not just the ViewModel.

But how can you prevent users of your code from using the implementation? There are a couple of ways to create this constraint.

One method is to make the implementation class internal. Then, provide a public Factory that always returns a reference to the Interface type. This way, the implementation never leaves the module.

Consider this BasicLocationProvider that might be used within a ViewModel:

class BasicLocationProvider: LocationProvider {
  val location: Location? = null

  fun refreshLocation() {
    // TODO trigger a location refresh in Android
  }
}

This code stores and exposes a location, and includes a method to refresh that location. You need to protect it from pesky developers who will use the implementation all over the code. First, declare the interface:

interface LocationProvider {
  fun refreshLocation()
  val location: Location?
}

Then, make sure that the class implements the interface and declare it internal:

internal class BasicLocationProvider: LocationProvider {
  override val location: Location?
    get() = null

  override fun refreshLocation() {
    // TODO trigger a location refresh in Android
  }
}

Lastly, create a Factory object that offers a LocationProvider to anyone interested:

object LocationProviderFactory {
  val locationProvider: LocationProvider =
    BasicLocationProvider()
}

With these modifications, the implementation of BasicLocationProvider is well-protected from users of the class.

Most Dependency Injection frameworks have an easier way of doing this. For example, in Dagger, you can expose the @Module, but keep the implementation classes internal.

When it comes to testing, constructors will receive mock objects that implement the interfaces. This makes testing the code easier. Testing frameworks and libraries let you easily create mock objects in order to control the input.

Testing Each Component

Now take a moment to review how you test each of the components covered so far.

  • View: This is the simplest component in MVVM. It doesn’t know about any other component and it doesn’t control anything. Testing the View is also simple. First, provide some controlled data just like a ViewModel would. Then, check the visual results. Voilà!
  • Model: This is usually simple to test also. The Model has no knowledge about the external world so you have complete control over it. All you have to do is to call “execute” methods on a Model object and then check the results with query method calls.
  • ViewModel: Testing this component is a little tricky. You have to mock the Model and call methods on the ViewModel. After that, check that the data available for the View is correct. You can also check that the methods called on the mock Model are correct. If the ViewModel depends on other components, you need to mock those as well. Still, testing the ViewModel is easier than testing the components of any of the previously mentioned design patterns.