In my last post, I’ve introduced the Android data binding support library which helps to get rid of unnecessary boiler-plate code related to view-model binding. In this post, I’d like to introduce you to the Android MVVM pattern (Model-View-ViewModel) which builds upon data binding functionality and helps to keep the architecture design of your applications clean and its parts clearly decoupled.
With MVVM, the ViewModel retrieves data from the Model when requested by the View via data binding mechanism. This makes Activities and Fragments really lightweight and your code becomes easily testable. Let’s have a look at the Android MVVM pattern in detail now.
Android MVVM pattern
One of the examples in the official documentation to the data binding support library presents a direct binding of properties from User data object to layout attributes. Specifically, this is the example code I want to discuss:
android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
Even though it’s perfectly correct to do syntactically, I’d argue against it. This logic belongs somewhere else than to the layout definition (we’ll soon find out that it belongs to the ViewModel class). Placing it here makes the testing and debugging of your code harder.
A better solution is to adopt an Android MVVM architectural pattern. The MVVM (Model-View-ViewModel) pattern abstracts the state and behaviour of the View, which allows the developer to separate the development of the user interface from the business logic. This is achieved by introducing a ViewModel layer in between the View and Model, that binds to the View and reacts to events.
The MVVM pattern comprises of three core components, each having a distinct role:
- Model: Data model representing the application business logic;
- View: The structure and appearance representation of the displayed content;
- ViewModel: Object linking the two together which deals with the view logic.
Other architectural patterns for Android development
MVVM is not the only architectural pattern to be used for development in Android. The most commonly used is probably MVC pattern (often not ideally implemented). Another approach gaining popularity lately is MVP which is quite similar to MVVM pattern in a way. Let’s have a look at what’s different about them.
MVC (Model-View-Controller)
MVC approach is quite common in Android development. It requires no additional architectural knowledge. On the other hand, it produces hundreds of lines of code in a single Activity or Fragment — so called God object. The code is messy and unstructured which often leads to bugs and is difficult to test.
The problem with this approach is that the Activity/Fragment classes have access to every other object and every action is done inside of their code. In this case, the Activity/Fragment acts as a Controller (click listeners and other event listeners) and a Model (business logic, connection to DB and REST API) at the same time. View layer is represented by the XML layout files.
MVP (Model-View-Presenter)
An example of a well-though-out architecture approach in Android is the MVP pattern. MVP allows to separate the Presentation layer from the business logic. The application is divided into three layers which can then be tested independently.
The Presenter acts as the middle man between the View and Model. It receives data from the model and returns it in a proper format to the View. It additionally decides what happens upon interaction with the View. The View contains a reference to the Presenter and the only thing it does is calling methods from the Presenter for each interface action (for example a button click). The Model is simply a provider of the data to display.
MVVM: Example application
We shall demonstrate the usage of Android MVVM pattern on the example application from my previous post on data binding. In short, the application displays a list of article items each containing a featured image of the article, its title, excerpt and two buttons navigating to hypothetical article comments and detail.
We modify the project by simplifying the Article model data class and creating a new ViewModel class acting as a bridge between the Model and View. The holding Activity doesn’t change much too:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity); RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); binding.contactList.setLayoutManager(layoutManager); List<Article> articles = new ArrayList<>(); /* data filling */ ArticleAdapter adapter = new ArticleAdapter(articles, this); binding.contactList.setAdapter(adapter); } }
Adapter
We implement an adapter for RecyclerView that uses data binding for each of its items.
public class ArticleAdapter extends RecyclerView.Adapter<ArticleAdapter.BindingHolder> { private List<Article> mArticles; private Context mContext; public ArticleAdapter(List<Article> mArticles, Context mContext) { this.mArticles = mArticles; this.mContext = mContext; } @Override public BindingHolder onCreateViewHolder(ViewGroup parent, int viewType) { ArticleItemBinding binding = DataBindingUtil.inflate( LayoutInflater.from(parent.getContext()), R.layout.article_item, parent, false); return new BindingHolder(binding); } @Override public void onBindViewHolder(BindingHolder holder, int position) { ArticleItemBinding binding = holder.binding; binding.setAvm(new ArticleViewModel(mArticles.get(position), mContext)); } @Override public int getItemCount() { return mArticles.size(); } public static class BindingHolder extends RecyclerView.ViewHolder { private ArticleItemBinding binding; public BindingHolder(ArticleItemBinding binding) { super(binding.contactCard); this.binding = binding; } } }
Model
With the introduction of ViewModel, the Article data model class becomes lighter and loses the View-Model binding logic. Now it’s only a POJO (Plain Old Java Object) with a constructors and getter and setter methods.
public class Article { private String title; private String excerpt; private boolean highlight; private String imageUrl; private int commentsNumber; private boolean read; /* constructor */ /* getters and setters */ }
ViewModel
The ViewModel class acts here as the middle man and communicates with both Model (in our case Article object) and View (defined by the layout XML file). It implements the Observable interface by extending the BaseObservable class and all the View-Model binding logic has moved into its code from the Article class:
public class ArticleViewModel extends BaseObservable { private Article mArticle; private Context mContext; public ArticleViewModel(Article mArticle, Context mContext) { this.mArticle = mArticle; this.mContext = mContext; } @Bindable public String getTitle() { return mArticle.getTitle(); } public void setTitle(String title) { mArticle.setTitle(title); notifyPropertyChanged(BR.title); } public int getCardBackgroundColor() { return mArticle.isHighlight() ? ContextCompat.getColor(mContext, R.color.highlight) : Color.parseColor("#ffffffff"); } public int getCommentsButtonVisibility() { return mArticle.getCommentsNumber() == 0 ? View.GONE : View.VISIBLE; } public int getCommentsNumber() { return mArticle.getCommentsNumber(); } public String getExcerpt() { return mArticle.getExcerpt(); } public String getImageUrl() { return mArticle.getImageUrl(); } @BindingAdapter({"image"}) public static void loadImage(ImageView view, String url) { Glide.with(view.getContext()).load(url).centerCrop().into(view); } public void setRead(boolean read) { // change title of already read article: if (read && !mArticle.isRead()) { setTitle("READ: " + getTitle()); } mArticle.setRead(read); } public View.OnClickListener onReadMoreClicked() { return new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(view.getContext(), "Opens article detail", Toast.LENGTH_SHORT).show(); setRead(true); } }; } public View.OnClickListener onCommentsClicked() { return new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(view.getContext(), "Opens comments detail", Toast.LENGTH_SHORT).show(); } }; } }
The clearer separation of View and Model layers in Android MVVM pattern can be observed for example in the getCommentsButtonVisibility method. Previously, the button visibility logic has been a part of the View (defined in XML). Now the visibility is decided upon in ViewModel and can be easily refactored and tested. Additionally, we no longer have to reference the View class as a variable from the layout file.
View (Layout XML files)
The only object the View layer has access to is now the ViewModel. Only through the ViewModel can the View get the data to present to the user. All the view logic (such as the card background colour logic for highlighted articles or button visibility) has been moved to the ViewModel class and View only calls the appropriate methods to get the result to show to the user.
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="avm" type="com.example.databindingblog.ArticleViewModel" /> </data> <android.support.v7.widget.CardView android:id="@+id/contact_card" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="20dp" app:cardBackgroundColor="@{avm.cardBackgroundColor}" app:cardCornerRadius="3dp" app:cardElevation="3dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/image" android:layout_width="match_parent" android:layout_height="200dp" android:layout_alignParentTop="true" app:image="@{avm.imageUrl}" /> <TextView android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@+id/image" android:layout_alignStart="@+id/image" android:layout_marginBottom="10dp" android:layout_marginEnd="20dp" android:layout_marginStart="20dp" android:ellipsize="end" android:lines="1" android:shadowColor="@android:color/black" android:shadowDx="4" android:shadowDy="4" android:shadowRadius="4" android:text="@{avm.title}" android:textColor="@android:color/white" android:textSize="25sp" android:textStyle="bold" /> <TextView android:id="@+id/excerpt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignStart="@+id/image" android:layout_below="@+id/image" android:layout_marginBottom="5dp" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginTop="10dp" android:lineSpacingMultiplier="1.2" android:text="@{avm.excerpt}" android:textAppearance="?android:attr/textAppearanceSmall" /> <Button android:id="@+id/read_more" style="@style/Widget.AppCompat.Button.Colored" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_below="@+id/excerpt" android:layout_marginBottom="10dp" android:layout_marginEnd="10dp" android:onClick="@{avm.onReadMoreClicked}" android:padding="10dp" android:text="Read more" /> <Button android:id="@+id/comments" style="@style/Widget.AppCompat.Button.Colored" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/excerpt" android:layout_marginBottom="10dp" android:layout_marginEnd="5dp" android:layout_toStartOf="@+id/read_more" android:onClick="@{avm.onCommentsClicked}" android:text="@{@plurals/numberOfComments(avm.commentsNumber, avm.commentsNumber)}" android:visibility="@{avm.commentsButtonVisibility}" /> </RelativeLayout> </android.support.v7.widget.CardView> </layout>
Conclusion
To conclude, if you decide to go with data binding (see my post on Android data binding) and plan to use it in more complex project, using the Android MVVM pattern is definitely the right way to go. It clearly separates the View and Model layers by introducing the ViewModel middle-man containing the view logic. Not only does the code become better structured, but it will also be much easier to test.
Here, you can download the whole example application project.