天天看點

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

  • ​​前言​​
  • ​​正文​​
  • ​​一、建立項目​​
  • ​​二、ViewModel使用​​
  • ​​① 綁定Activity​​
  • ​​② 頁面布局繪制​​
  • ​​③ 實作登入​​
  • ​​二、LiveData使用​​
  • ​​① 可修改資料​​
  • ​​② 資料觀察​​
  • ​​三、DataBinding使用​​
  • ​​① 單向綁定​​
  • ​​② 雙向綁定​​
  • ​​四、源碼​​

前言

  MVVM架構出來已經有一段時間了,現在也有很多的項目運用了MVVM架構,是以也不算是很新的東西,但是從個人的角度來說我希望寫出來,因為每年都會有新的Android開發工程師進入,一些架構的使用都是封裝好的,或者寫的很進階,剛開始不容易看懂,是以我的想法是寫一個簡單易懂的MVVM架構,并且在這個上面去加入Jetpack的元件,當然了,我技術比較菜,大佬要是看見了高擡貴手。

正文

  MVVM架構是有由來的,這個其實說來話長了,還得從最開始的Android 視圖、UI來說起。最開始的時候Android編寫頁面,裡面的業務邏輯和UI處理都在Activity中,很符合這樣一個圖。

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

一、建立項目

而最開始的解耦架構是MVC,Model + View + Controller。

  Model (模型層) 儲存資料的狀态,比如資料存儲,網絡請求。同時還與View 存在一定的耦合,可以通過觀察者模式通知 View 狀态的改變來讓view 更新。

  View (視圖層) 同時響應使用者的互動行為并觸發 Controller 的邏輯,View 還有可能修改Model 的狀态 以使其與 Model 同步,View 還會在model 中注冊 model 事件的改變。以此來重新整理自己并展示給使用者。

  Control (控制層)控制器由View 根據使用者行為觸發并響應來自view 的使用者互動,然後根據view 的事件邏輯來修改對應的Model, Control 并不關心 View 如何展示 相關資料或狀态,而是通過修改 Model 來實作view 的資料的重新整理。

而中間的架構是MVP,Model + View + Presenter。相當于對MVC架構的進一步更新,解耦。不過也有缺點,額外增加了大量的接口、類,不友善進行管理,是以關于MVP的話就還有一個Contract要去處理。

Contract 如其名,是一個契約,将Model、View、Presenter 進行限制管理,友善後期類的查找、維護。

presenter - 邏輯處理層對UI的各種業務事件進行相應處理。不與View産生直接關系。

最後是我們目前最流行的架構MVVM,Model + View + ViewModel。解耦更徹底,如果說之前是藕斷絲連的話,現在就是一刀兩斷。

ViewModel:關聯層,将Model和View進行綁定,隻做和業務邏輯相關的工作,不涉及任何和UI相關的操作,不持有控件引用,不更新UI。

View隻做和UI相關的工作,不涉及任何業務邏輯,不涉及操作資料,不處理資料。UI和資料嚴格的分開。

好了,說了這麼多理論的東西,下面進入實操環節,先說明開發環境,我使用的Android Studio是4.2.1,API版本30,gradle 版本6.7.1,JDK8,電腦Win10。

首先建立一個項目,命名為MVVM-Demo。

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

本文的主要目标是ViewModel 和 DataBinding。

  從Google的官方說明來看,ViewModel 類旨在以注重生命周期的方式存儲和管理界面相關的資料。ViewModel 類讓資料可在發生螢幕旋轉等配置更改後繼續留存。DataBinding資料綁定庫是一種支援庫,借助該庫,您可以使用聲明性格式(而非程式化地)将布局中的界面元件綁定到應用中的資料源。

看懂了之後首先在項目中,啟用DataBinding,找到app子產品下的build.gradle,在android{}閉包下添加如下代碼:

//啟用DataBinding
    buildFeatures {
        dataBinding true
    }      

然後點選AS右上角的Sync Now進行工程配置同步,而ViewModel不需要做什麼就可以使用了。

二、ViewModel使用

  ViewModel的優勢在于生命周期和資料持久化,那麼它就适用于Activity和Fragment,其次就是異步回調,不會造成記憶體洩漏,再次就是對View層和Model層進行隔離,是兩者不存在耦合性,是以你可以知道ViewModel在整個MVVM架構中的重要性了。

① 綁定Activity

在MVVM的架構中,每一個Activity都應該對應一個ViewModel,而現在我們有一個MainActivity,是以可以建立一個viewmodels包,包下建立一個MainViewModel類,表示與MainActivity進行綁定。

public class MainViewModel extends ViewModel {

}      

注意這裡要繼承ViewModel,雖然現在裡面什麼都沒有的,但後面使用的時候會進行增加,下面先将我們的MainActivity與MainViewModel進行綁定。

回到MainActivity中,修改代碼如下圖所示;

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

現在我們的MainActivity和MainViewModel就綁定起來了。ViewModel是資料持久化的,因為對于一些變量就可以直接放在ViewModel當中,而不再放在Activity中,可以根據一個實際的需求來進行。

② 頁面布局繪制

比如我現在有一個登入的功能要去實作,要怎麼去對資料進行處理呢?

在ViewModel中定義兩個變量

public String account;
    public String pwd;      

賬号和密碼這當然是最基本的兩個資料了,下面我們修改一下activity_main.xml中的布局,代碼如下:

<?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="賬号" />
    </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="密碼"
            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="登  錄"
        app:cornerRadius="12dp" />

</LinearLayout>      

③ 實作登入

下面回到MainActivity中,增加代碼如下圖所示:

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

乍一看好像沒啥不同的,無非就是給mainViewModel中的兩個變量賦了值。不過這裡有一個資料持久化的内容在裡面,怎麼證明呢?看一下下面這個GIF圖

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

  這個圖可能有一些黑屏的地方,因為我在給自己的手機做橫豎屏切換的時候,手機錄屏好像有一點問題,不過沒事。因為這個結果是對的,那就是資料持久化,因為我們知道手機在切換螢幕的時候Activity是會重新建立的,是以如果我們的資料是放在Activity中,那麼切換螢幕之後就會重置,輸入框也不會有值,但是通過ViewModel去儲存輸入框的值就不同了,雖然你的Activity在切換螢幕的時候銷毀并且重新建立了,但是我的MainModel依然穩定,是以我才能在橫屏的時候也登陸,這樣不會造成資料丢失。

二、LiveData使用

  LiveData是用來做什麼的?資料變化感覺,也就是說如果我一個頁面中對一個TextView進行多次指派的話,可以通過LiveData來操作,隻需要在值改變的時候進行設定就好了,可以簡化頁面上的代碼。下面舉一個實際的例子來說明。依然是之前那個登入頁面,不過需要修改一下MainViewModel中的變量,如下:

① 可修改資料

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

  請注意這裡使用的是MutableLiveData,表示值的内容開變動,而LiveData是不可變的。<>中的是泛型,你可以直接将一個對象放進去,當對象的内容有改動時,通知改變就可以了,現在這麼寫是為了友善了解。下面進入MainActivity中,首先我們改變一下布局activity_main.xml在按鈕的下面再加如下代碼

<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"/>      

② 資料觀察

然後回到MainActivity中,修改代碼如下圖所示:

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

  上面的圖中從上往下有四處标注,我們從下面的兩處标注來看,首先在給MainViewModel中的account指派時,采用了MutableLiveData的setValue()的方式,還有一種方式是postValue(),這裡要注意一點setValue()隻能在主線程中調用,postValue()可以在任何線程中調用。pwd也是一樣的,然後在最後一處标注的地方,對MainViewModel中的account和pwd進行資料觀察,當這兩個值有改變時通知頁面最新的值,這裡用了lambda表達式進行了一次簡化,實際的代碼是這樣的。

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

下面我們運作一下:

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

三、DataBinding使用

  Android的DataBinding在已經内置了,是以隻需要在app子產品的build.gradle中開啟就可以使用了。DataBinding,顧名思義就是資料綁定,可以看到現在的三個元件都與資料有關系,ViewModel資料持有,LiveData資料觀察、DataBinding資料綁定。

① 單向綁定

  而DataBinding的綁定有兩種方式:單向資料綁定和雙向資料綁定。舉個例子:比如我手機上收到一個通知,我需要顯示通知的文字内容在頁面上,這就是單向綁定,而我頁面上的文字内容改變也重新發一個通知出去,這就是雙向綁定。可以了解為A和B進行互動。A發消息,B要做出反應。B發消息,A也要相應改變。最常用的就是當我Model中的資料改變時,改變頁面上的值。這個是單向綁定。下面我們可以建立一個使用者User對象,代碼如下:

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();//通知改變 所有參數改變
    }

    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;
    }
}      

這裡我繼承了BaseObservable,注意它在androidx.databinding包下。然後我們的資料是需要顯示在頁面上的,而之前是通過Activity擷取xml中的控件,然後顯示資料在控件上,而現在有了DataBinding,可以直接和xml的中資料進行綁定,這看起來和JS比較像。下面我們對activity_main.xml進行改變。改變後代碼如下:

<?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">

    <!--綁定資料-->
    <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="賬号" />
        </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="密碼"
                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="登  錄"
            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>      

這裡我在最外層加了一個layout标簽,然後将原來的布局放在layout裡面,再增加一個資料源,也就是user對象,然後再底部的兩個tv_account和tv_pwd兩個TextView中的text屬性中綁定了user對象中的屬性值。當然這樣還沒有完成,最後一步是在MainActivity中去進行綁定的。

進入MainActivity。在onCreate方法中,先将其他的代碼注釋掉。

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

然後修改待明日如下圖所示

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

  這裡要注意一點,DataBindingUtil.setContentView傳回的是一個ViewDataBinding對象,這個對象的生成取決于你的Activity,例如MainActivity,就會生成ActivityMainBinding。然後再通過生成的ActivityMainBinding去設定要顯示在xml中控件的值。是以你會看到我完全沒有去findViewById,然後控件再去設定這個setText。還有一點就是當你使用了DataBinding之後就不需要去手動findViewById了,通過編譯時技術會生成駝峰命名的對象,如上圖的btnLogin、etAccount、etPwd。上圖的代碼就是通過更改資料然後通知到xml做更改,初始化的修改時admin、123456。然後再通過輸入框去修改。我将會輸入study、666,然後點選登入按鈕,也會将輸入框的資料顯示在TextView上,這樣是否會省去很多不必要的繁瑣工作呢?下面運作一下:

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

② 雙向綁定

  雙向綁定是建立在單向綁定的基礎上,實際的開發中用到雙向綁定的地方并沒有單向綁定多,雙向綁定舉一個例子,在輸入框輸入資料時候直接将資料源中的資料進行改變,這裡會用到ViewModel和LiveData。下面進行雙向綁定的使用,修改一下MainViewModel,代碼如下:

public class MainViewModel extends ViewModel {

    public MutableLiveData<User> user;

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

下面修改User類,這裡面做了一些改變

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);//隻通知改變的參數
    }

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

    public void setPwd(String pwd) {
        this.pwd = pwd;
        notifyPropertyChanged(BR.pwd);//隻通知改變的參數
    }

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

不同于notifyChange()改變某一個參數,某一個對象都會通知,現在notifyPropertyChanged()就具有針對性,隻通知對應屬性改變。之前在activity_main.xml中的data标簽中是使用的User,現在我們改成ViewModel,順便把布局調整一下,代碼如下:

<?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">

    <!--綁定資料-->
    <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="賬号" />
        </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="密碼"
                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="登  錄"
            app:cornerRadius="12dp" />

    </LinearLayout>
</layout>      

這裡要注意點的地方有幾個,第一個是資料源,這裡綁定的是ViewModel,那麼相對應的ViewModel中的資料資料都可以拿到。

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

第二個就是響應的地方,通過這種方式去顯示ViewModel中對象的變量資料在控件上。這裡我把這兩個TextView放到輸入框的上方

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

第三個地方,也是雙向綁定的意義,就是UI改變資料源。我們都知道當輸入框輸入時,text屬性值會改變為輸入的資料,而@={viewModel.user.account}就是将輸入的資料直接指派給資料源。這樣在Activity中我們将不需要去進行輸入框的處理,減少了耦合。

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

下面讓我們回到MainActivity中。修改代碼後如下:

private ActivityMainBinding dataBinding;
    private MainViewModel mainViewModel;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //資料綁定視圖
        dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        mainViewModel = new MainViewModel();
        //Model → View
        User user = new User("admin", "123456");
        mainViewModel.getUser().setValue(user);
        //擷取觀察對象
        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, "請輸入賬号", Toast.LENGTH_SHORT).show();
                return;
            }
            if (mainViewModel.user.getValue().getPwd().isEmpty()) {
                Toast.makeText(MainActivity.this, "請輸入密碼", Toast.LENGTH_SHORT).show();
                return;
            }
            Toast.makeText(MainActivity.this, "登入成功", Toast.LENGTH_SHORT).show();
        });
    }      

下面運作一下:

Android MVVM架構搭建(一)ViewModel + LiveData + DataBinding

繼續閱讀