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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM