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<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<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的時候差不多,有區別的地方只有兩點:
- ObservableField替換成LiveData
- 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。
關於我
微信公眾號:
如果你覺得這片文章對你有所啟發的話,可以關注我的微信公眾號哦