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 3 of 4 of this article. Click here to view the first page.

Best practices in Testing MVVM

Testing is straightforward when you apply a good pattern. There are a few secrets to testing MVVM, but here’s the best approach:

  • Start testing the ViewModel. Test each one of the public methods, going through as many cases as you can. Not just the obvious paths, but real edge cases that you’re sure are never going to happen. Trust me: users and future developers will reach those edge cases. :]
  • If something isn’t testable, wrap it! Create an interface that contains all of the methods you need from the untestable class. Create a class that implements that interface and instantiate the untestable class you wanted to use inside of it. Then, inside your class, call the methods from the untestable class. This is called the Proxy design pattern.

Now you have an interface you can use in your app that’s easily replaceable with a mock object. This way, you can “throw” untestable dependencies outside your class.

You should keep Android SDK framework components and external libraries at arms-length. By wrapping them this way, you make sure you don’t spread dependencies all over the code. You use the interface you’ve created and keep the frameworks and libraries in a single place.

Keep your tests short and make sure they are repeatable.

It’s not your responsibility to test the framework. You can’t control the framework, so there’s no point in testing it. If your Model only forwards methods to another library like Room, there’s not much to do in terms of testing. You can add tests once your Model needs to do something else, like filtering or computing.

Test your View with different device configurations. Mock the ViewModel to generate all the states that a View could receive.

Adding Android’s Architecture Components

So far, you’ve seen the main MVVM components and discussed testing them. But what about the Binder layer? The Android SDK provides Data Binding to implement a Binder.

Using DataBinding

Data binding is a technique that connects data sources with consumers and keeps them in sync. Android’s Data Binding Library lets the developer keep the UI in the layout XML files and generates code from them. The library also does the heavy lifting of synchronizing the View with data that comes from the ViewModel.

To add data binding to your project, you need to add the following to your app’s build.gradle file:

android {
  ...
  dataBinding {
    enabled = true
  }
}

Now, your layout XMLs will need to have the following structure:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="user"
            type="com.example.User" />
    </data>
    <ConstraintLayout... /> <!-- UI layout's root element -->
</layout>

To transform XML from the default structure to a data binding one, you need to wrap the root layout, in this case a ConstraintLayout, inside a layout tag. Then, at the top of the layout, just inside the layout tag, you will need to add a data tag, containing the objects that are used for binding.

Note: Once you have data binding enabled and sync your project, Android Studio offers a shortcut for transforming any layout into a data binding one. Select the root element of your layout and press option + return. A pop-up appears with the option “Convert to data binding layout”. Select that and the layout will add the data and layout tags automatically.

The UI will reflect every change in the ViewModel via the variables inside the data tag. In the example, there is a variable named user.

You have to tell the UI elements what property they need. For example:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.example.User"/>
   </data>
   <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
   android:id="@+id/firstName"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
  android:id="@+id/lastName"/>
   </LinearLayout>
</layout>
</xml>

Here there are two text views that are bound to the user.firstName and user.lastName values.

You will still need to add different listeners to your UI and forward those events to the ViewModel. Lucky for you, the DataBinding library can also help with that. The library generates a binding object for accessing the UI elements:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)
}

In the above, you create a binding value using the generated binding object.

Adding Android’s own ViewModel

Creating your own ViewModel class is great, but Android Jetpack offers more. Jetpack’s ViewModel has its own lifecycle, managed by the library. This means that the library will keep the ViewModel when the OS recreates the Activity. You also don’t need to synchronize the UI state.

To begin, create a class that inherits from Jetpack’s ViewModel:

class ProfileViewModel : ViewModel() {
  val uiModel: MutableLiveData<UiModel> by lazy {
    MutableLiveData< UiModel >()
  }
}

Then, in your Activity or Fragment, add this:

val model = ViewModelProviders.of(this).get(ProfileViewModel::class.java)

If the activity is re-created, it receives the same ProfileViewModel instance as before. When the owner activity finishes, the framework calls the ViewModel object’s onCleared() method. That’s when the ViewModel is really destroyed.

Loose Coupling with LiveData

Jetpack also provides the LiveData class. LiveData is just the Observer pattern improved with lifecycle awareness. Most of the problems in Android development come from lifecycle handling. Developers that use LiveData are less likely to have:

To use LiveData, first create your LiveData object in your ViewModel:

  • Memory leaks
  • Crashes
  • Outdated UI
class ProfileViewModel : ViewModel() {
  val uiModel: MutableLiveData<UiModel> by lazy {
    MutableLiveData< UiModel >()
  }
}

After that, connect your View to this LiveData. In your Activity or Fragment, after you’ve created the ViewModel, add this:

viewModel.uiModel.observe(this, Observer<UiModel>{ uiModel ->
  // update UI
})

The Database in the Room

The database we discussed earlier named Room is also a Jetpack component. Using Room in your Android app is straightforward. There are three types of classes that you need to write:

  • Entities
  • Data access objects
  • Databases

Entities

Entities are POJOs (Plain-Old-Java-Objects) with annotations for column names and a primary key:

@Entity
data class User(
  @PrimaryKey var uid: Int,
  @ColumnInfo(name = "first_name") var firstName: String?,
  @ColumnInfo(name = "last_name") var lastName: String?
)

Data Access Objects

Usually, each entity has its own data access object (DAO). The DAO implements the CRUD operations, as well as other helpful methods.

@Dao
interface UserDao {
  // Create
  @Insert
  fun insert(user: User)

  // Read
  @Query("SELECT * FROM user")
  fun getAll(): List<User>

  // Update
  @Update
  fun update(user: User)

  // Delete
  @Delete
  fun delete(user: User)
}

Here, UserDao, has been given each of the CRUD operations tagged with the corresponding annotation.

The best thing about Jetpack components is that they work great together. For example, Room can also return LiveData objects.

// Read
@Query("SELECT * FROM user")
fun getAll(): LiveData<List<User>>

Cool, huh? This method is lifecycle aware and will propagate changes in a reactive way.

Next up for using Room, you need to create the database.

Database

The database interface needs to know what entities it will have. This lets Room generate the appropriate classes and methods.

@Database(entities = arrayOf(User::class), version = 1)
abstract class MvvmDatabase : RoomDatabase() {
  abstract fun userDao(): UserDao
}

Room also handles the versioning of your database. That’s what the second parameter version is for. It will help you run migrations when you upgrade the database.

Lastly, you need to initialize the database when the app starts:

val db = Room.databaseBuilder(
  applicationContext,
  MvvmDatabase::class.java, "mvvm-article"
).build()

You provide the builder a context, the database class, and a name for the database file, e.g. “mvvm-article”.