Now that Google has -finally- started working on a Data Binding engine, it’s time for me to start changing the way I use the Presentation Model pattern.
Why use Presentation Model?
It’s been talked a lot about this pattern, so I’m not going to re-explain the wheel. You can find useful references here, here, here and here. Very briefly, the main motivations to use this pattern are:
- Moves the presentation logic into specific components. This is specially important on Android, where views (Activities and Fragments, in our case) can get quite large.
- Improves testability and reusability, as controllers (Presenters or ViewModels) can be written in pure Java.
- Decouples the model from the view, so our business logic is completely independent.
Why Model View ViewModel?
I have been using MVP (and therefore Presenters) for about a year. It’s been great, as my code became more testable, and my activities/fragments got smaller. You can have a look at this gist by Christian Panadero to get an idea about how I and others use the Presentation Model with Passive View. But I still find that there is one more step forward: the MVVM pattern.
MVVM is a variation of the MVP (Model View Presenter) with Passive View. Although they are very similar (both use a Controller -Presenter/ViewModel- to wire the view and the model, and tell the view what to do), the main difference with MVP is that MVVM encourages the use of Data Binding.
Why is this important? Because Data Binding wires your data and your view in a way that the view responds automatically to data changes, and viceversa. Besides, the use of a Data Binding engine avoids a lot of boilerplate.
Show me the code!
I’m going to show how this works through a very simple example. This is not exactly the way I code my projects, because I wanted to focus on the MVVM pattern and remove any other complexity (DI, Commands, Executors, etc.). If you see some rudimentary code between the ViewModels and the Model, don’t get scared, it’s meant to be like that for this example 🙂
The basic idea
The example consists of a very simple layout where the user age is displayed. Every time it is refreshed, a new age is generated and the view says whether the user is young (< 30) or not and changes the display depending on that.
The core of this implementation focus on three entities:
- ViewModels: Act as the controllers between the Model and the View. They receive events from the view, decide what to do, call the model and tell the view what to do when the answer is available. They are 100% Java code.
- Binding Views: Implement the binding between the data and the view. They make use of the Data Binding engine. In this example, I make use of the @Bindable annotations.
- Views (activities/fragments): Implement the view and set up the Data Binding stuff needed by the engine.
Although MVVM is helpful, I think it’s important to generalize a bit, so I have created a few abstractions:
ViewModel
This abstract class contains some code needed by all the ViewModels, as attaching/detaching the view or accessing the Binding View.
public abstract class ViewModel<E, V> { private final BindingView<E> bindingView; private V view; public ViewModel(BindingView<E> bindingView) { this.bindingView = bindingView; } public void attachView(V view) { this.view = view; } public void detachView() { this.view = null; } protected BindingView<E> getBindingView() { return bindingView; } protected V getView() { return view; } }
BindingView
This interface provides a bind method needed by the ViewModel classes to update the views.
public interface BindingView<T> { void bind(T model); }
BaseBindingView
This abstract class uses the BaseObservable class to perform a notification when the data is bound.
public abstract class BaseBindingView<T> extends BaseObservable implements BindingView<T> { @Override public void bind(T model) { bindData(model); notifyDataChanged(); } protected abstract void bindData(T model); private void notifyDataChanged() { notifyChange(); } }
The User example
Initially, the user information needs to be retrieved and displayed. This is done in the UserDetailViewModel class.
public class UserDetailViewModel extends ViewModel<User, UserDetailView> implements UserProvider.Callback { private final UserProvider userProvider; private User user; public UserDetailViewModel(UserBindingView userBindingView, UserProvider userProvider) { super(userBindingView); this.userProvider = userProvider; this.userProvider.setCallback(this); } ... public void onRefresh() { new Thread(new Runnable() { @Override public void run() { userProvider.provideUser(); } }).start(); } @Override public void onUserRetrieved(User user) { this.user = user; getBindingView().bind(user); getView().enableUpdateUserAge(); } ... }
The user retrieved is bound to the view by using the UserBindingView interface. This is its implementation.
public class UserBindingViewImp extends BaseBindingView<User> implements UserBindingView { private String name; private int age; private boolean young; @Override protected void bindData(User user) { this.name = user.getName(); this.age = user.getAge(); this.young = user.isYoung(); } @Bindable public String getName() { return name; } @Bindable public int getAge() { return age; } @Bindable public boolean isYoung() { return young; } }
The use of @Bindable sets the binding, so every time the bind method is called, the view is automatically updated using these getters.
The view is just an interface used by the ViewModel to make the view perform the actions. They should be widget-independent but small enough. For example, enableUpdateUserAge() method will be used to enable the corresponding widget(s).
public interface UserDetailView { void enableUpdateUserAge(); void disableUpdateUserAge(); }
Finally, the view is implemented by the UserDetailActivity. Using the Data Binding engine avoids having to implement more code to update the view (this is done by BindingView#bind). The only things that need to be implemented are the actions the view performs, as enableUpdateUserAge. Also, the view is responsible for setting up the binding provided by the Android library, using DataBindingUtil#setContentView. This is the code:
public class UserDetailActivity extends AppCompatActivity implements UserDetailView { private UserDetailViewModel userDetailViewModel; private Button refreshButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { final UserBindingViewImp userBindingView = BindingViewFactory.createUserBindingView(); ActivityUserDetailBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_user_detail); viewDataBinding.setUserBindingView(userBindingView); refreshButton = (Button) findViewById(R.id.refresh_button); refreshButton.setOnClickListener(new android.view.View.OnClickListener() { @Override public void onClick(android.view.View v) { userDetailViewModel.onUpdateUserAge(); } }); userDetailViewModel = new UserDetailViewModel(userBindingView, new UserProvider()); userDetailViewModel.attachView(this); } } @Override protected void onResume() { super.onResume(); userDetailViewModel.onRefresh(); } @Override protected void onDestroy() { userDetailViewModel.detachView(); super.onDestroy(); } @Override public void enableUpdateUserAge() { runOnUiThread(new Runnable() { @Override public void run() { refreshButton.setVisibility(View.VISIBLE); } }); } @Override public void disableUpdateUserAge() { refreshButton.setVisibility(View.GONE); } }
The drawing logic is done in the activity layout, using the binding engine facilities. The UserBindingView object bound to the user model contains all the information needed to draw the view.
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <import type="android.graphics.Color"/> <import type="android.view.View"/> <variable name="userBindingView" type="com.srodrigo.databindingtest.modules.user.UserBindingViewImp"/> </data> <LinearLayout xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{userBindingView.name}"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@{userBindingView.young ? Color.GREEN : Color.RED}" android:text="@{String.valueOf(userBindingView.age)}" tools:text="31"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:text='@{userBindingView.name + " Is young!"}' android:visibility="@{userBindingView.young ? View.VISIBLE : View.GONE}" tools:text="Is young!"/> </LinearLayout> <Button android:id="@+id/refresh_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Update age"/> </LinearLayout> </layout>
This is the Github repository with the whole example.
Conclusion
This is how I migrated from MVP to MVVM to take advantage of the Data Binding. This is still a very simple and premature example, but it gives me a good feeling about the possibility of using it in the future on production code when the final version of the library is released.
Any comments/improvements/suggestions will be welcome 🙂