Android MVVM framework building ViewModel + LiveData + DataBinding

preface

   MVVM framework has been out for some time. Now many projects use MVVM framework, so it is not very new. But from a personal point of view, I hope to write it, because new Android development engineers enter every year. The use of some frameworks is encapsulated or highly written, which is not easy to understand at the beginning, Therefore, my idea is to write a simple and easy to understand MVVM framework and add Jetpack components to it. Of course, my technology is relatively good. If the boss sees it, he will raise his hand.

text

  MVVM framework has its origin. In fact, it's a long story, and we have to start with the initial Android view and UI. At the beginning, Android wrote the page, in which the business logic and UI processing are in the Activity, which is in line with such a diagram.

1, Create project

The initial decoupling framework is MVC, Model + View + Controller.

   Model (Model layer) saves the state of data, such as data storage and network request. At the same time, there is a certain coupling with the View. The View can be updated by notifying the change of View state through the observer mode.

   view (view layer) responds to the user's interaction behavior and triggers the logic of the Controller. View may also modify the state of the model to synchronize it with the model. View will also register the changes of model events in the model. To refresh themselves and show them to users.

   the Control layer controller is triggered by the View according to the user behavior and responds to the user interaction from the View, and then modifies the corresponding Model according to the event logic of the View. The Control does not care how the View displays the relevant data or status, but realizes the refresh of the View data by modifying the Model.

The middle framework is MVP, Model + View + Presenter. It is equivalent to further upgrading and decoupling the MVC framework. However, there are also disadvantages. A large number of interfaces and classes are added, which is inconvenient for management. Therefore, there is a Contract to deal with MVP.

Contract, as its name suggests, is a contract that manages the constraints of Model, View, and Presenter to facilitate later class discovery and maintenance.

presenter - the logical processing layer handles various business events of the UI accordingly. Not directly related to View.

Finally, our most popular framework MVVM, Model + View + ViewModel. The decoupling is more complete. If the coupling was broken before, it is now a clean break.

ViewModel: association layer, which binds Model and View, only does work related to business logic, does not involve any UI related operations, does not hold control references, and does not update UI.

View only does UI related work and does not involve any business logic, operation data or data processing. UI and data are strictly separated.

Well, after talking about so many theoretical things, let's go to the practical operation link and explain the development environment first. I use Android Studio 4.2.1, API version 30,gradle version 6.7.1, JDK8 and computer Win10.

First create a project named MVVM demo.

The main objectives of this article are ViewModel and DataBinding.

  according to the official description of Google, the ViewModel class is designed to store and manage interface related data in a life-cycle manner. The ViewModel class allows data to remain after configuration changes such as screen rotation. The DataBinding data binding library is a support library that allows you to bind interface components in a layout to data sources in an application in a declarative format rather than programmatically.

After understanding, first enable DataBinding in the project, find build.gradle under the app module, and add the following code under the android {} closure:

	//Enable DataBinding
    buildFeatures {
        dataBinding true
    }

Then click Sync Now in the upper right corner of the AS to synchronize the project configuration, and the ViewModel can be used without doing anything.

2, ViewModel usage

   the advantage of ViewModel lies in life cycle and data persistence, so it is applicable to Activity and Fragment. The second is asynchronous callback, which will not cause memory leakage. The third is to isolate the View layer and Model layer. There is no coupling between the two. Therefore, you can know the importance of ViewModel in the whole MVVM framework.

① Bind Activity

In the MVVM framework, each Activity should correspond to a ViewModel, and now we have a MainActivity, so we can create a viewmodels package and a MainViewModel class under the package, indicating binding with MainActivity.

public class MainViewModel extends ViewModel {

}

Note that the ViewModel should be inherited here. Although there is nothing in it now, it will be added later. Let's bind our MainActivity to the MainViewModel first.

Go back to MainActivity and modify the code as shown in the following figure;

Now our MainActivity and MainViewModel are bound. ViewModel is data persistent, because some variables can be directly placed in ViewModel instead of in Activity, which can be carried out according to an actual demand.

② Page layout drawing

For example, I now have a login function to implement. How do I process the data?

Define two variables in the ViewModel

	public String account;
    public String pwd;

Of course, account and password are the two most basic data. Let's modify the activity_ The layout code in main.xml is as follows:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="32dp"
    tools:context=".MainActivity">

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/et_account"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/white"
            android:hint="account number" />
    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.textfield.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="12dp">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/et_pwd"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/white"
            android:hint="password"
            android:inputType="textPassword" />
    </com.google.android.material.textfield.TextInputLayout>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_margin="24dp"
        android:insetTop="0dp"
        android:insetBottom="0dp"
        android:text="Sign in"
        app:cornerRadius="12dp" />

</LinearLayout>

③ Realize login

Now go back to MainActivity and add the code as shown in the following figure:

At first glance, it seems to be no different. It is nothing more than assigning values to two variables in mainViewModel. However, there is a data persistence content in it. How to prove it? Take a look at the GIF diagram below

  there may be some black screen in this picture, because when I switch the horizontal and vertical screens of my mobile phone, there seems to be a problem with the mobile phone recording screen, but it's okay. Because this result is correct, that is, data persistence, because we know that the Activity will be re created when the mobile phone switches the screen. Therefore, if our data is placed in the Activity, it will be reset after switching the screen, and the input box will not have a value, but it is different to save the value of the input box through ViewModel, Although your Activity is destroyed and recreated when switching screens, my MainModel is still stable, so I can log in when the screen is horizontal, so I won't lose data.

2, LiveData usage

  what is LiveData used for? Data change perception, that is, if I assign values to a TextView multiple times on a page, I can operate through LiveData. I just need to set it when the value changes, which can simplify the code on the page. Here is a practical example to illustrate. It is still the previous login page, but you need to modify the variables in MainViewModel as follows:

① Modifiable data

	public MutableLiveData<String> account = new MutableLiveData<>();
    public MutableLiveData<String> pwd = new MutableLiveData<>();

   please note that MutableLiveData is used here, indicating that the content of the value changes, while LiveData is immutable. < > You can directly put an object into it. When the content of the object is changed, you can notify the change. Now it is written for ease of understanding. Let's go to MainActivity. First, let's change the layout activity_main.xml add the following code below the button

	<TextView
        android:id="@+id/tv_account"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <TextView
        android:id="@+id/tv_pwd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

② Data observation

Then go back to MainActivity and modify the code as shown in the following figure:


The graph above is marked from top to bottom. We have annotated from the following two parts. First, when we assign account to MainViewModel, we use MutableLiveData setValue(), and one way is postValue(). Here we should note that setValue() can only be called in the main thread, and postValue() can be invoked in any thread. pwd is the same. Then, in the last marked place, observe the data of account and pwd in mainviewmodel. When these two values change, notify the latest value of the page. Here, lambda expression is used for simplification. The actual code is like this.

Let's run:

3, DataBinding usage

   Android DataBinding has been built in, so you only need to open it in build.gradle of app module. DataBinding, as its name implies, is data binding. You can see that the three components are related to data. ViewModel data holding, LiveData data observation and DataBinding data binding.

① Unidirectional binding

  there are two ways to bind data binding: one-way data binding and two-way data binding. For example, when I receive a notification on my mobile phone, I need to display the text content of the notification on the page, which is one-way binding, and when the text content on my page changes, I also send a new notification, which is two-way binding. It can be understood that a and B interact. A sends a message and B responds. B sends a message and a changes accordingly. The most common is to change the value on the page when the data in my Model changes. This is a one-way binding. Next, we can create a new User object with the following code:

public class User extends BaseObservable {

    private String account;
    private String pwd;

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
        notifyChange();//Notify all parameter changes
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    public User(String account, String pwd) {
        this.account = account;
        this.pwd = pwd;
    }
}

Here, I inherit BaseObservable. Note that it is under the Android x.DataBinding package. Then, our data needs to be displayed on the page. Previously, we obtained the control in XML through Activity, and then displayed the data on the control. Now with DataBinding, we can directly bind the data in XML, which looks like JS. Let's talk about activity_main.xml. The changed code is as follows:

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <!--Binding data-->
    <data>
        <variable
            name="user"
            type="com.llw.mvvm.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="32dp"
        tools:context=".MainActivity">

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_account"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:hint="account number" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_pwd"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:hint="password"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:layout_margin="24dp"
            android:insetTop="0dp"
            android:insetBottom="0dp"
            android:text="Sign in"
            app:cornerRadius="12dp" />

        <TextView
            android:id="@+id/tv_account"
            android:text="@{user.account}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_pwd"
            android:text="@{user.pwd}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </LinearLayout>
</layout>

Here, I add a layout label to the outermost layer, then put the original layout in the layout, add a data source, that is, the user object, and then two TVs at the bottom_ Account and tv_pwd the text attribute in the two textviews is bound with the attribute value in the user object. Of course, this is not finished yet. The last step is to bind in MainActivity.

Enter MainActivity. In the onCreate method, comment out the other code first.

Then modify to be tomorrow, as shown in the figure below

    note here that DataBindingUtil.setContentView returns a ViewDataBinding object. The generation of this object depends on your Activity. For example, MainActivity will generate ActivityMainBinding. Then set the value of the control to be displayed in xml through the generated ActivityMainBinding. So you'll see that I didn't go to findViewById at all, and then the control set this setText. Another point is that you don't need to manually findViewById after using DataBinding. Hump named objects will be generated through compile time technology, such as btnLogin, etAccount and etPwd in the above figure. The code in the figure above is to change the data and then notify the xml to make the change. The initial modification is admin, 123456. Then modify it through the input box. I will enter study and 666, and then click the login button. The data in the input box will also be displayed on TextView. Will this save a lot of unnecessary tedious work? Run the following:

② Bidirectional binding

   two way binding is based on one-way binding. In actual development, two-way binding is not used as much as one-way binding. For example, two-way binding directly changes the data in the data source when entering data in the input box. ViewModel and LiveData are used here. Here is the use of two-way binding. Modify the MainViewModel. The code is as follows:

public class MainViewModel extends ViewModel {

    public MutableLiveData<User> user;

    public MutableLiveData<User> getUser(){
        if(user == null){
            user = new MutableLiveData<>();
        }
        return user;
    }
}

Next, modify the User class, and some changes have been made

public class User extends BaseObservable {

    public String account;
    public String pwd;

    @Bindable
    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
        notifyPropertyChanged(BR.account);//Notify only changed parameters
    }

    @Bindable
    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
        notifyPropertyChanged(BR.pwd);//Notify only changed parameters
    }

    public User(String account, String pwd) {
        this.account = account;
        this.pwd = pwd;
    }
}

Unlike notifyChange() changing a parameter, an object will be notified. Now notifyPropertyChanged() is targeted and only notifies the corresponding property change. Previously in activity_ The User is used in the data tag in main.xml. Now we change it to ViewModel and adjust the layout. The code is as follows:

<?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"
    xmlns:tools="http://schemas.android.com/tools">

    <!--Binding data-->
    <data>
        <variable
            name="viewModel"
            type="com.llw.mvvm.viewmodels.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="32dp"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/tv_account"
            android:text="@{viewModel.user.account}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:layout_marginBottom="24dp"
            android:id="@+id/tv_pwd"
            android:text="@{viewModel.user.pwd}"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_account"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:text="@={viewModel.user.account}"
                android:hint="account number" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_pwd"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/white"
                android:text="@={viewModel.user.pwd}"
                android:hint="password"
                android:inputType="textPassword" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_login"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:layout_margin="24dp"
            android:insetTop="0dp"
            android:insetBottom="0dp"
            android:text="Sign in"
            app:cornerRadius="12dp" />

    </LinearLayout>
</layout>

There are several points to pay attention to here. The first is the data source. The ViewModel is bound here, so the data in the corresponding ViewModel can be obtained.

The second is the response place. In this way, the variable data of the object in the ViewModel is displayed on the control. Here I put these two textviews above the input box

The third place, which is also the meaning of two-way binding, is that the UI changes the data source. We all know that when the input box is entered, the text attribute value will change to the input data, and @ = {viewModel.user.account} is to assign the input data directly to the data source. In this way, we will not need to process the input box in the Activity, reducing the coupling.

Let's go back to MainActivity. The modified code is as follows:

private ActivityMainBinding dataBinding;
    private MainViewModel mainViewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //Data binding view
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        mainViewModel = new MainViewModel();
        //Model → View
        User user = new User("admin", "123456");
        mainViewModel.getUser().setValue(user);
        //Get observation object
        MutableLiveData<User> user1 = mainViewModel.getUser();
        user1.observe(this, user2 -> dataBinding.setViewModel(mainViewModel));

        dataBinding.btnLogin.setOnClickListener(v -> {
            if (mainViewModel.user.getValue().getAccount().isEmpty()) {
                Toast.makeText(MainActivity.this, "Please enter the account number", Toast.LENGTH_SHORT).show();
                return;
            }
            if (mainViewModel.user.getValue().getPwd().isEmpty()) {
                Toast.makeText(MainActivity.this, "Please input a password", Toast.LENGTH_SHORT).show();
                return;
            }
            Toast.makeText(MainActivity.this, "Login succeeded", Toast.LENGTH_SHORT).show();
        });
    }

Run the following:

I found that there seems to be something wrong with my mobile phone recording screen. When I click the second input box, the recording screen will be black. So this GIF is very short. Please download the source code to run the specific effect.

4, Source code

GitHub: MVVM-Demo

Tags: LiveData

Posted on Thu, 28 Oct 2021 15:49:15 -0400 by Fari