SkillAgentSearch skills...

Mv2m

Android MVVM lightweight library based on Android Data Binding

Install / Use

/learn @fabioCollini/Mv2m
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

mv<sup>2</sup>m

Android MVVM lightweight library based on Android Data Binding

Circle CI JitPack Android Arsenal

The goal of mv<sup>2</sup>m is to simplify JVM testing of an Android application. The demo module contains a simple example of usage of mv<sup>2</sup>m library. The official app of the Italian blog cosenonjaviste.it is a more complex example (built using Dagger and RxJava). This repository contains the mv<sup>2</sup>m porting of android-testing codelab repository.

Mv2m is available on JitPack, add the JitPack repository in your build.gradle (in top level dir):

repositories {
    jcenter()
    maven { url "https://jitpack.io" }
}

and the dependency in the build.gradle of the module:

dependencies {
    //core module
    compile 'com.github.fabioCollini.mv2m:mv2m:0.4.1'
    //RxJava support
    compile 'com.github.fabioCollini.mv2m:mv2mrx:0.4.1'
    //RecyclerView support
    compile 'com.github.fabioCollini.mv2m:mv2mrecycler:0.4.1'
}

Components of mv<sup>2</sup>m

Model

A Model class contains the data used to populate the user interface using Data Binding library. It's saved automatically in Actvity/Fragment state, for this reason it must implement Parcelable interface.

View

The View is an Activity or a Fragment, the user interface is managed using Android Data Binding. You need to extend ViewModelActivity or ViewModelFragment class and implement createViewModel method returning a new ViewModel object.

ViewModel

The ViewModel is a subclass of ViewModel class, it is automatically saved in a retained fragment to avoid the creation of a new object on every configuration change. The ViewModel is usually the object bound to the layout using the Data Binding framework. It manages the background tasks and all the business logic of the application.

mv2m class diagram

JUnit tests

The ViewModel is not connected to the View, for this reason it's easily testable using a JVM test. The demo module contains some JUnit tests for ViewModel classes.

Activity

Sometimes an Activity object is required to perform some operations, for example you need an Activity object to show a SnackBar message or to start another Activity. In this case you can create a new class with some methods with an ActivityHolder parameter:

public class SnackbarMessageManager {

    @Override public void showMessage(ActivityHolder activityHolder, int message) {
        if (activityHolder != null) {
            Snackbar.make(activityHolder.getActivity().findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show();
        }
    }
}

An implementation of this class can be referenced from a ViewModel object. ViewModel class contains a protected ActivityHolder field containing a reference to the current Activity that can be used as parameter (it's automatically updated every configuration change). In this way it's easy to show a SnackBar from a ViewModel.

In a JVM test SnackbarMessageManager object can be mocked to avoid dependencies on Android framework.

ViewModel argument

ViewModel class has two generic parameters: the type of the argument and the type of the Model class. The argument can be used when the ViewModel needs an argument, for example in a detail ViewModel the argument is the id of the object you have to show. The argument must be an object that can be written in a Bundle (a primitive type, a Serializable or a Parcelable). Using an ActivityHolder object you can start a new Activity passing an argument:

activityHolder.startActivity(NoteActivity.class, noteId);

ViewModel class contains a protected field argument that can be used to load the model data based on the argument.

In case you use Fragments as View objects you can use ArgumentManager.instantiateFragment method to create a new Fragment and set the argument on it.

Work in progress

This library (and the Android Data Binding library) is still a beta version; feedbacks, bug reports and pull requests are welcome!

Talk is cheap. Show me the code.

Let's see how to use mv<sup>2</sup>m to implement a simple example: a currency converter Activity.

mv2m demo

The Model class contains two fields, one is bound with the input EditText and the other with the output TextField:

public class CurrencyConverterModel implements Parcelable {

    public ObservableString input = new ObservableString();

    public ObservableString output = new ObservableString();

    //Parcelable implementation...
}

Using RateLoader class it's possible to load a rate from an external source (for example a REST service), in this first implementation it executes everything synchronously:

public class RateLoader {

    private static RateLoader INSTANCE = new RateLoader();

    private RateLoader() {
    }

    public static RateLoader instance() {
        return INSTANCE;
    }

    public float loadRate() {
        //...
    }
}

All the business logic is in the CurrencyConverterViewModel class, it extends ViewModel class defining two generic parameters: Void (we don't need any argument) and the Model class.

public class CurrencyConverterViewModel extends ViewModel<Void, CurrencyConverterModel> {

    private RateLoader rateLoader;

    public CurrencyConverterViewModel(RateLoader rateLoader) {
        this.rateLoader = rateLoader;
    }

    @NonNull @Override public CurrencyConverterModel createModel() {
        return new CurrencyConverterModel();
    }

    public void calculate() {
        String inputString = model.input.get();
        float input = Float.parseFloat(inputString);
        model.output.set(new DecimalFormat("0.00").format(input * rateLoader.loadRate()));
    }
}

We use the dependency injection on the constructor to manage a reference to a RateLoader object. The createModel method is overridden to create and return a new Model object.

The ViewModel class has no Android dependencies, it can be tested easily using a JUnit test:

@RunWith(MockitoJUnitRunner.class)
public class CurrencyConverterViewModelTest {

    @Mock RateLoader rateLoader;

    @InjectMocks CurrencyConverterViewModel viewModel;

    @Test
    public void testConvertCurrency() {
        when(rateLoader.loadRate()).thenReturn(2f);

        CurrencyConverterModel model = viewModel.initAndResume();
        model.input.set("123");
        viewModel.calculate();

        assertThat(model.output.get()).isEqualTo("246.00");
    }
}

In this JUnit test we use Mockito to create a mock of RateLoader object, in this way the test is repeatable because it's not dependent on external sources.

Data binding library is used to bind ViewModel object to the layout:

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

    <data>
        <variable name="viewModel"
                  type="it.cosenonjaviste.demomv2m.core.currencyconverter1.CurrencyConverterViewModel"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp">

        <EditText
            android:id="@+id/input"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:binding="@{viewModel.model.input}">
            <requestFocus/>
        </EditText>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:layout_marginTop="16dp"
            android:text="@string/convert"
            app:onClick="@{viewModel.calculate}"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.model.output}"/>

    </LinearLayout>
</layout>

The app:binding and app:onClick are custom attributes, this post explains how to create them.

The Activity is very simple, it extends ViewModelActivity, overrides createViewModel method, creates the layout and binds it to the ViewModel:

public class CurrencyConverterActivity extends ViewModelActivity<CurrencyConverterViewModel> {

    @Override public CurrencyConverterViewModel createViewModel() {
        return new CurrencyConverterViewModel(RateLoader.instance());
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        CurrencyConverterBinding binding = DataBindingUtil.setContentView(this, R.layout.currency_converter);
        binding.setViewModel(viewModel);
    }
}

Using an Espresso test we can verify that the binding works correctly:

public class CurrencyConverterActivityTest {

    @Rule public ViewModelActivityTestRule<Void> rule = new ViewModelActivityTestRule<>(CurrencyConverterActivity.class);

    private RateLoader rateLoader;

    @Befor

Related Skills

View on GitHub
GitHub Stars182
CategoryDevelopment
Updated7mo ago
Forks20

Languages

Java

Security Score

87/100

Audited on Aug 19, 2025

No findings