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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
MVVM and DataBinding: Android Design Patterns
30 mins
- Getting Started
- Understanding Design Patterns
- Using Architectural Design Patterns
- Common Architectural Design Patterns in Android
- MVC and MVP
- Taking the Next Step: MVVM
- Understanding MVVM Components
- Defining the Role of the View
- Defining the Model
- Finding the ViewModel’s Place in MVVM
- Improving Testability
- Hiding Behind Interfaces and Dependency Injection
- Testing Each Component
- Best practices in Testing MVVM
- Adding Android’s Architecture Components
- Using DataBinding
- Adding Android’s own ViewModel
- Loose Coupling with LiveData
- The Database in the Room
- Searching For More
- Clean Architecture
Understanding MVVM Components
MVVM is now one of the most loved patterns out there, as it has had plenty of time to mature. The web community adopted the pattern after the Microsoft team formalized it in 2005. It eventually made its way into every UI-based framework.
One key advantage of MVVM is that it offers the right amount of decoupling. Another good thing is that the learning curve is similar to the other patterns.
MVVM has three main components: Model, View, and ViewModel.
Unlike MVP and MVC, there’s a fourth component: The Binder. This is the mechanism that links the Views to the ViewModels.
The Binder is usually handled by the platform or a third party library, so the developer doesn’t have to write it. For Android, you have the DataBinding library at your disposal.
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.
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.