1. 程式人生 > >Android開發利器之Data Binding Compiler V2 —— 搭建Android MVVM完全體的基礎

Android開發利器之Data Binding Compiler V2 —— 搭建Android MVVM完全體的基礎

原創宣告: 該文章為原創文章,未經博主同意嚴禁轉載。

前言: Android常用的架構有:MVC、MVP、MVVM,而MVVM是唯一一個官方提供支援元件的架構,我們可以通過Android lifecycle系列元件、DataBinding或者通過組合兩者的形式來打造一個強大的MVVM架構。而DataBinding Compiler V2就是為了解決目前的MVVM架構中的缺點而誕生的。

Data Binding和LiveData的相容問題

在DataBinding Compiler V1的環境下,DataBinding和LiveData是無法相容的。這句話是什麼意思呢?我們先來看看平時我們使用DataBinding的程式碼片段。

Data Binding

佈局程式碼片段

<data>  
    <variable  
        name= "text"  
        type="android.databinding.ObservableField&lt;String>"/>  
</data>  
  
<TextView  
    android:layoutwidth="matchparent"  
    android:layoutheight="40dp"  
    android:text=“@{text}“  
    />

注:xml不能直接使用‘<’所以我們需要使用轉義符:"<"
使用程式碼片段

XXXBinding binding = ...  
private final ObservableField<String> text = new ObservableField<>();  
binding.setText(text)  
text.set(" hello word ")`

上面的程式碼片段是DataBinding的簡單使用方法。

LiveData

我們知道LiveData是Google官方推出的生命週期感知的資料包裝元件,用來搭建MVVM框架有天然的優勢,能很好協調控制層與展示層生命週期不一致的問題(這裡是指View層與ViewModel層)下面我們來看下使用LiveData更新UI的程式碼片段。

ViewModel程式碼片段

public class TestModel extends ViewModel {  
    private final MutableLiveData<String> text = new MutableLiveData<>();  
  
    public LiveData<String> getText() {  
        return text;  
    }  
}  

View層程式碼片段

viewModel. getText().observe(this, observe -> {  
    tvText.setText(observe);  
});  

當我們在ViewModel中呼叫 text.postValue(obj)方法時,UI層的observe方法就會收到回撥,通過tvText.setText(observe);這句程式碼來更新tvText。

例如,我們可以在ViewModel中通過下面的程式碼來更新UI層

text.posValue("hello word !")  

可以看出,無論是使用DataBinding還是LiveData,都能實現View層和ViewModel層解耦的目的,並且能ViewModel層中的資料變化來實現View層的更新,這就是我們常說資料驅動檢視

資料驅動檢視:只要資料變化, 就重新渲染檢視

ObservableField與LiveData

我們知道DataBinding是通過ObservableField來實現資料的雙向繫結的,而ObservableField本質上就是一個被觀察者,而我們的xml佈局檔案和就是觀察者,當ObservableField產生變化是會通知我們的佈局檔案更新佈局(觀察者模式)。
ObservableField如何實現通知佈局檔案更新的原理我們這裡先不深入討論,這裡筆者只給出一個結論,ObservableField被View層(這裡指我們的xml佈局檔案)以弱引用的方式引用,當ObservableField更新時,會通過監聽器通知View層,並且ObservableField是對View層生命週期不敏感的。所以通過ObservableField實現資料雙向繫結並不是一個完美的方案。

我們可以考慮使用LiveData來實現雙向繫結。
我們先來回顧一下監聽LiveData方法:

viewModel. getText().observe(this, observe -> {  
    tvText.setText(observe);  
});  

非常簡單,只在呼叫LiveData的observe,設定一個Observer
回撥監聽器就可以了。

那麼上文提到的Databinding與LiveData不相容是指什麼呢?
從上面的分析我們可以看出ObservableField與LiveData的使用方式完全是完全不一樣的,ObservableField可以通過直接在佈局檔案中設定實現雙向繫結。而LiveData必須通過程式碼設定監聽器,並且需要手動呼叫待更新的控制元件才能實現控制元件的更新。就是說LiveData只能通知UI層有資料需要更新,更新後的資料是什麼,但是並不能自動幫你實現View的更新。並且當View層的資料更新後,LiveData也沒辦法自動獲取View層的更新。

例如:在使用EditText的時候,要獲取EditText的改變,需要呼叫EditText的getText方法,而ObservableField只需要呼叫get()方法即可

LiveData在Data Binding Compiler V1下是無法使用類似ObservableField的方式實現資料繫結的(單向也不行),這就是筆者所說的DataBinding與LiveData不相容。
當我們使用DataBinding與Lifecycle組合搭建MVVM框架的時候,需要根據業務的具體需要來選擇使用LiveData還是ObservableField。類似下面的程式碼:

public final ObservableBoolean dataLoading = new ObservableBoolean(false);  
  
private final MutableLiveData<Void> mTaskUpdated = new MutableLiveData <>();  

但是實際開發的時候,我們往往無法在ObservableField與LiveData中作出很好的選擇,因為它們的優缺點都太明顯了。
我們總結一下ObservableField與LiveData的優缺點。
 ObservableField
優點:使用方便,能快速實現雙向繫結
缺點:使用弱引用的方式與View層,並且不能根據View層的生命週期來發送通知

LiveData
優點:能根據View層的生命週期來發送通知事件
缺點:使用麻煩,與View層耦合大,並且不支援資料與View繫結

Data Binding Compiler V2

我們要說的主角就是,Data Binding Compiler V2 。

什麼是Data Binding Compiler呢?

Data Binding Compiler是Data Binding的編譯器,它的主要作用就是編譯出我們在使用Data Binding時需要使用的輔助程式碼。例如:ActivityxxxBinding格式的類檔案就是由Data Binding Compiler編譯生成的,並且ObservableField資料雙向繫結也是由編譯器編譯的程式碼提供支援的。
Data Binding Compiler V2是Data Binding的第二代編譯器,這個編譯器和V1編譯器最大的不同就是:V1編譯器只支援ObservableField系列的資料包裝類與View層的雙向繫結,而V2編譯器能讓LiveData支援Data Binding雙向繫結。
我們可以看看在V2編譯器環境下LiveData實現雙向繫結的程式碼片段:
佈局程式碼片段

<data>  
    <variable  
    name="text"  
    type="android.arch.lifecycle.LiveData&lt;String>"/>  
</data>  
  
<TextView  
    android:layoutwidth="matchparent"  
    android:layoutheight="40dp"  
    android:text=“@{text}“  
    />

使用程式碼片段

XXXBinding binding = ...  
binding.setLifecycleOwner(this);  
MutableLiveData<String> text = new MutableLiveData<>();  
binding.setText(text);  
text.postValue(" hello word ");

可以看出,在Data Binding Compiler V2 環境下,使用LiveData實現雙向繫結的方法和使用Observable實現雙向繫結的方法基本山是一樣的。通過Data Binding Compiler V2我們能把LiveData不能實現雙向繫結和使用麻煩的缺點徹底解決,並且還能保留LiveData能感知View層生命週期的優點保留下來。

如何使用Data Binding Compiler V2?

環境配置

要使用Data Binding Compiler V2 的話,可能需要升級一下開發環境,需要的配置如下。

  • Android Studio 版本需要升級到3.1 Canary 6以上
  • gradle版本需要升級到 alpha06以上
  • gradle-wrapper.properties中的distributionUrl需要改成gradle-4.4
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip  
  • 需要在gradle.properties檔案中啟用databinding V2
android.databinding.enableV2=true  

當我們配置完後,重新clear一下專案就可以開啟Data Binding Compiler V2了。

使用方法

我們以一個模擬登陸的例子來簡單介紹如何使用Data Binding Compiler V2。

資料類

public class Account {  
    private MutableLiveData<String> accountNum = new MutableLiveData<>();  
    private MutableLiveData<String> password = new MutableLiveData<>();  
  
    Account(String accountNum, String password){  
        this.accountNum.setValue(accountNum);  
        this.password.setValue(password);  
    }  
  
    public MutableLiveData<String> getAccountNum(){  
        return accountNum;  
    }  
  
    public MutableLiveData<String> getPassword(){  
        return password;  
    }  
  
}  

xml佈局檔案

<?xml version="1.0" encoding="utf-8"?>  
<layout xmlns:tools="http://schemas.android.com/tools"  
    xmlns:app="http://schemas.android.com/apk/res-auto">  
  
    <data>  
        <variable  
            name="viewModel"  
            type="tang.com.databindingcompilerv2.login.LoginViewModel"/>  
  
        <import type="android.view.View"/>  
    </data>  
  
    <android.support.constraint.ConstraintLayout  
        xmlns:android="http://schemas.android.com/apk/res/android"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent">  
  
        <android.support.design.widget.TextInputLayout  
            android:id="@+id/til_account_num"  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:layout_marginEnd="8dp"  
            android:layout_marginLeft="8dp"  
            android:layout_marginRight="8dp"  
            android:layout_marginStart="8dp"  
            android:layout_marginTop="8dp"  
            app:layout_constraintEnd_toEndOf="parent"  
            app:layout_constraintStart_toStartOf="parent"  
            app:layout_constraintTop_toTopOf="parent">  
  
            <android.support.design.widget.TextInputEditText  
                android:id="@+id/et_account_num"  
                android:layout_width="match_parent"  
                android:layout_height="wrap_content"  
                android:text="@={viewModel.account.accountNum}"  
                android:hint="@string/account_prompt"/>  
  
        </android.support.design.widget.TextInputLayout>  
  
        <android.support.design.widget.TextInputLayout  
            android:id="@+id/til_password"  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:layout_marginEnd="8dp"  
            android:layout_marginLeft="8dp"  
            android:layout_marginRight="8dp"  
            android:layout_marginStart="8dp"  
            app:layout_constraintEnd_toEndOf="parent"  
            app:layout_constraintStart_toStartOf="parent"  
            app:layout_constraintTop_toBottomOf="@+id/til_account_num">  
  
            <android.support.design.widget.TextInputEditText  
                android:id="@+id/et_password"  
                android:layout_width="match_parent"  
                android:layout_height="wrap_content"  
                android:inputType="textWebPassword"  
                android:text="@={viewModel.account.password}"  
                android:hint="@string/password_prompt" />  
  
        </android.support.design.widget.TextInputLayout>  
  
        <android.support.v7.widget.AppCompatButton  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:layout_marginBottom="8dp"  
            android:layout_marginEnd="8dp"  
            android:layout_marginLeft="8dp"  
            android:layout_marginRight="8dp"  
            android:layout_marginStart="8dp"  
            android:text="@string/login"  
            android:onClick="@{viewModel.login}"  
            app:layout_constraintBottom_toBottomOf="parent"  
            app:layout_constraintEnd_toEndOf="parent"  
            app:layout_constraintStart_toStartOf="parent" />  
  
        <ProgressBar  
            android:id="@+id/progressBar"  
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:layout_marginBottom="8dp"  
            android:layout_marginEnd="8dp"  
            android:layout_marginLeft="8dp"  
            android:layout_marginRight="8dp"  
            android:layout_marginStart="8dp"  
            android:layout_marginTop="8dp"  
            app:layout_constraintBottom_toBottomOf="parent"  
            app:layout_constraintEnd_toEndOf="parent"  
            app:layout_constraintStart_toStartOf="parent"  
            app:layout_constraintTop_toTopOf="parent"  
            app:isVisible="@{viewModel.isLoading}"  
            />  
  
        <TextView  
            android:id="@+id/tv_prompt"  
            android:layout_width="match_parent"  
            android:layout_height="40dp"  
            android:layout_marginEnd="8dp"  
            android:layout_marginLeft="8dp"  
            android:layout_marginRight="8dp"  
            android:layout_marginStart="8dp"  
            android:text="@{viewModel.loginPrompt}"  
            app:layout_constraintEnd_toEndOf="parent"  
            app:layout_constraintStart_toStartOf="parent"  
            app:layout_constraintTop_toBottomOf="@+id/til_password" />  
  
    </android.support.constraint.ConstraintLayout>  
  
</layout>  

ViewModel

public class LoginViewModel extends ViewModel {  
  
    private static final String TAG = "LoginViewModel";  
  
    private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>();  
    private final MutableLiveData<Account> account = new MutableLiveData<>();  
    private final MutableLiveData<String> loginPrompt = new MutableLiveData<>();  
  
    public LoginViewModel(){  
        account.postValue(new Account("",""));  
        isLoading.postValue(false);  
    }  
  
    public void login(View view){  
        String loginMsg =  "accountNum = " + Objects.requireNonNull(account.getValue()).getAccountNum().getValue()  
                + "\npassword = " + Objects.requireNonNull(account.getValue()).getPassword().getValue();  
        Log.d(TAG,"\n正在登陸中....\n"  
               + loginMsg);  
        loginPrompt.postValue("正在登陸賬號:" + Objects.requireNonNull(account.getValue()).getAccountNum().getValue());  
        isLoading.postValue(true);  
            new Handler().postDelayed(() -> {  
                Log.d(TAG,"登陸成功....\n");  
                isLoading.postValue(false);  
                Intent intent = new Intent(view.getContext(), MainActivity.class);  
                intent.putExtra("hello", loginMsg);  
                view.getContext().startActivity(intent);  
                loginPrompt.postValue("");  
            }, 2000);  
  
    }  
  
    public MutableLiveData<Boolean> getIsLoading(){  
        return isLoading;  
    }  
  
    public Account getAccount(){  
        return account.getValue();  
    }  
  
    public MutableLiveData<String> getLoginPrompt() {  
        return loginPrompt;  
    }  
}  

Activity

public class MainActivity extends AppCompatActivity {  
  
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);  
        binding.setHello(getIntent().getStringExtra("hello") + "\n hello word !");  
        binding.setLifecycleOwner(this);  
  
    }  
}  

到這裡,我們就能愉快地Data Binding Compiler V2了。
從測試程式碼可以看出,程式碼和我們使用Data Binding Compiler V1的時候差不多,有區別的地方只有兩點:

  1. ObservableField替換成LiveData
  2. binding物件需要呼叫setLifecycleOwner(LifecycleOwner lifecycleOwner
    )設定lifecycleOwner物件。

示例程式碼

筆者在GitHub上面建立了一個專案,以後所有的文章的測試DEMO都會上傳到這個專案上,有興趣的讀者可以關注下。
這篇文章的示例在專案中的todoDatabinding檔案下。

專案結構如圖所示:

其中databindingcompilerv1為Data Binding Compiler V1下的示例程式碼
其中databindingcompilerv2為Data Binding Compiler V2下的示例程式碼

Data Binding Compiler V2 示例程式碼

小結

Data Binding Compiler V2主要是解決了Data Binding不能感知View層生命週期的問題。
在Android開發中我們的控制層(這裡指ViewModel)的生命週期和View層元件的生命週期是不能保持一致的,大多數情況下,控制層的生命週期會比View層長。例如,我們發起網路請求的時候,在請求回撥之前View有被銷燬的可能,如果在View被銷燬後控制層再更新View層,這個時候我們就會遇到討厭的NPE異常。Lifecycle系列元件的主要功能就是使控制層能夠感知View層的生命週期。而Data Binding Compiler V2則是為了使Data Binding能夠使用Lifecycle中的LiveData從而獲得感知生命週期的能力,即達成Data Binding 的lifecycle-aware。

關於我

GitHub

微信公眾號:
如果你覺得這片文章對你有所啟發的話,可以關注我的微信公眾號哦