The whole point of MVVM is to separate layers containing logic from the view layer.

On Android we can use the DataBinding Library to help us with this and make most of our logic Unit-testable without worrying about Android dependencies.

In this example I’ll show the central components for a stupid simple App that does the following:

Let’s start with the view layer:

activity_main.xml:

If you’re unfamiliar with how DataBinding works you should probably take 10 minutes to make yourself familiar with it. As you can see, all fields you would usually update with setters are bound to functions on the viewModel variable.

If you’ve got a question about the android:visibility or app:textColor properties check the ‘Remarks’ section.

<layout
   xmlns:android="<http://schemas.android.com/apk/res/android>"
   xmlns:app="<http://schemas.android.com/apk/res-auto>"
   xmlns:tools="<http://schemas.android.com/tools>">

   <data>

       <import type="android.view.View" />

       <variable
           name="viewModel"
           type="de.walled.mvvmtest.viewmodel.ClickerViewModel"/>
   </data>

   <RelativeLayout
       android:id="@+id/activity_main"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:padding="@dimen/activity_horizontal_margin"

       tools:context="de.walled.mvvmtest.view.MainActivity">

       <LinearLayout
           android:id="@+id/click_counter"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_centerHorizontal="true"
           android:layout_marginTop="60dp"
           android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"

           android:padding="8dp"

           android:orientation="horizontal">

           <TextView
               android:id="@+id/number_of_clicks"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               style="@style/ClickCounter"

               android:text="@{viewModel.numberOfClicks}"
               android:textAlignment="center"
               app:textColor="@{viewModel.counterColor}"

               tools:text="8"
               tools:textColor="@color/red"
           />

           <TextView
               android:id="@+id/static_label"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_marginLeft="4dp"
               android:layout_marginStart="4dp"
               style="@style/ClickCounter"

               android:text="@string/label.clicks"
               app:textColor="@{viewModel.counterColor}"
               android:textAlignment="center"

               tools:textColor="@color/red"
           />
       </LinearLayout>

       <TextView
           android:id="@+id/message"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_below="@id/click_counter"
           android:layout_centerHorizontal="true"
           android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"

           android:text="@{viewModel.labelText}"
           android:textAlignment="center"
           android:textSize="18sp"

           tools:text="You're bad and you should feel bad!"
       />

       <Button
           android:id="@+id/clicker"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_below="@id/message"
           android:layout_centerHorizontal="true"
           android:layout_marginTop="8dp"
           android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"

           android:padding="8dp"

           android:text="@string/label.button"

           android:onClick="@{() -> viewModel.onClickIncrement()}"
       />

       <android.support.v4.widget.ContentLoadingProgressBar
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginTop="90dp"
           android:layout_centerHorizontal="true"
           style="@android:style/Widget.ProgressBar.Inverse"
           android:visibility="@{viewModel.loadingVisible ? View.VISIBLE : View.GONE}"

           android:indeterminate="true"
       />

   </RelativeLayout>

</layout>

Next the model layer. Here I have:

Also I define here a ‘state of excitement’ that is dependent on the number of clicks. This will later be used to update color and message on the View.

It is important to note that there are no assumptions made in the model about how the state might be displayed to the user!

ClickerModel.java

import com.google.common.base.Optional;

import de.walled.mvvmtest.viewmodel.ViewState;

public class ClickerModel implements IClickerModel {

    private int numberOfClicks;
    private Excitement stateOfExcitement;

    public void incrementClicks() {
        numberOfClicks += 1;
        updateStateOfExcitement();
    }

    public int getNumberOfClicks() {
        return Optional.fromNullable(numberOfClicks).or(0);
    }

    public Excitement getStateOfExcitement() {
        return Optional.fromNullable(stateOfExcitement).or(Excitement.BOO);
    }

    public void restoreState(ViewState state) {
        numberOfClicks = state.getNumberOfClicks();
        updateStateOfExcitement();
    }

    private void updateStateOfExcitement() {
        if (numberOfClicks < 10) {
            stateOfExcitement = Excitement.BOO;
        } else if (numberOfClicks <= 20) {
            stateOfExcitement = Excitement.MEH;
        } else {
            stateOfExcitement = Excitement.WOOHOO;
        }
    }
}