Jetpack AAC 系列文章:
“終于懂了“系列:Jetpack AAC完整解析(一)Lifecycle 完全掌握!
“終于懂了“系列:Jetpack AAC完整解析(二)LiveData 完全掌握郑叠!
“終于懂了“系列:Jetpack AAC完整解析(三)ViewModel 完全掌握秋泳!
“終于懂了“系列:Jetpack AAC完整解析(四)MVVM 架構(gòu)探索!
“終于懂了“系列:Jetpack AAC完整解析(五)DataBinding 架構(gòu)完善倡缠!
前面四篇介紹了Jetpack 架構(gòu)組件中的 基礎(chǔ)組件 以及它們的綜合應(yīng)用:Jetpack MVVM 架構(gòu)模式哨免,到這里已經(jīng)基本滿足標(biāo)準(zhǔn)化開發(fā)了。但 Jetpack 架構(gòu)組件 除了 Lifecycle毡琉、LivaData铁瞒、ViewModel妙色,還有:
- WorkManager桅滋,用于管理后臺(tái)工作的任務(wù),即使應(yīng)用退出或重啟時(shí)身辨。
- Paging丐谋,分頁庫,按需加載部分?jǐn)?shù)據(jù)煌珊。
- Startup号俐,用于App啟動(dòng)速度優(yōu)化的庫,但只適用于庫開發(fā)者定庵, 郭霖這篇有詳細(xì)介紹吏饿。
- DataStore,用于替換SharedPreferences蔬浙,目前還處于Alpha階段猪落。
- DataBinding,將布局中的界面組件直接綁定到數(shù)據(jù)源畴博,提供雙向綁定笨忌,及高級綁定適配能力。
- ViewBinding俱病,用于替代findViewById官疲,而DataBinding也包含ViewBinding的能力袱结。
- Room,實(shí)現(xiàn)本地存儲(chǔ) 數(shù)據(jù)庫管理途凫,支持LiveData垢夹。
目前,就學(xué)習(xí)使用的必要性和庫的功能性 來說维费,WorkManager棚饵、Paging、Startup都是非必須的掩完,DataStore還未正式發(fā)布噪漾,ViewBinding的能力也包含在DataBinding中。Room且蓬,實(shí)際 功能和性能 同GreenDAO類似欣硼,有個(gè)好處是支持LivaData,但已使用GreenDao的項(xiàng)目恶阴,也不必切換為Room了诈胜。
DataBinding是比較有爭議的一個(gè)庫,這也是本篇的重點(diǎn)冯事,相信會(huì)帶你 重新認(rèn)識(shí) 被誤解的 DataBinding焦匈。
一、重新認(rèn)知 DataBinding
DataBinding的使用方法昵仅,參考官方文檔就可以缓熟,介紹地很詳細(xì)了,這里就不再搬運(yùn)摔笤。(另外還找到一個(gè)慕課網(wǎng)的視頻很不錯(cuò):入門篇够滑、高級篇)
1.1 DataBinding 的本質(zhì)
應(yīng)該不少人和我以前一樣,對 DataBinding 的認(rèn)知就是 在xml中寫邏輯:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{!isFemale? user.name + ":男士": user.name + ":女士"}'/>
看到 xml 中 使用三元表達(dá)式 來計(jì)算view需要的值吕世,然后就認(rèn)為:“ DataBinding 不好用彰触!在xml中寫表達(dá)式邏輯,出錯(cuò)了debug不了啊命辖,邏輯寫在xml里面的話 xml 就承擔(dān)了 Presenter/ViewModel 的職責(zé) 變得混亂了啊况毅。”
如果是把邏輯寫在xml中尔艇,確實(shí)如是:xml中是不能調(diào)試的尔许、職責(zé)上確實(shí)是混亂了。
但漓帚,這就是 DataBinding 的本質(zhì)了嗎母债?
1.1.1 DataBinding 以前
在 DataBinding 出現(xiàn)以前,想要改變視圖 就要引用該視圖:
TextView textView = findViewById(R.id.sample_text);
if (textView != null && viewModel != null) {
textView.setText(viewModel.getUserName());
}
而要引用該視圖就要先判空,textView 和 viewModel 都不能為空毡们。textView為啥要判空呢迅皇?一種情況是 R.id.sample_text是定義在在其他頁面中;一種情況是存在控件存在差異的 橫衙熔、豎 兩種布局登颓,如橫屏存在此 textView 控件僧叉,而豎屏沒有栋荸,那么就需要對其做判空處理。
App內(nèi)頁面和控件數(shù)量繁多簿姨,一個(gè)控件可能會(huì)多處調(diào)用痢甘,這就會(huì)有出現(xiàn)空指針的可能喇嘱,那如何完全避免呢?
1.1.2 數(shù)據(jù)綁定
DataBinding塞栅,含義是 數(shù)據(jù)綁定者铜,即 布局中的控件 與 可觀察的數(shù)據(jù) 進(jìn)行綁定。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"/>
布局中這個(gè)TextView是實(shí)實(shí)在在 存在的放椰,就不需要判空了作烟。而user是否為空 DataBinding也會(huì)自動(dòng)處理:在表達(dá)式 @{user.name} 中,如果 user 為 Null砾医,則為 user.name 分配默認(rèn)值 null拿撩。
并且,當(dāng)該 user.name 被 set 新值時(shí)如蚜,被綁定了該數(shù)據(jù)的控件即可獲得通知和刷新压恒。換言之,在使用 DataBinding 后怖亭,唯一的改變是涎显,你無需手動(dòng)調(diào)用視圖來 set 新狀態(tài),你只需 set 數(shù)據(jù)本身兴猩。
所以,DataBinding 并非是 將 UI 邏輯搬到 XML 中寫 導(dǎo)致而難以調(diào)試 早歇,只負(fù)責(zé)綁定數(shù)據(jù)倾芝, UI 控件 與 其需要的 終態(tài)數(shù)據(jù) 進(jìn)行綁定。 終態(tài)數(shù)據(jù)是指 UI 控件 直接需要的數(shù)據(jù)(UI數(shù)據(jù))箭跳,string值晨另、int值等,而不是一段邏輯(不然就叫 LogicBinding了 谱姓,雖然DataBinding支持邏輯表達(dá)式)借尿。
明確了 DataBinding 的 職責(zé)邊界后 應(yīng)該知道了:原本的邏輯代碼 該怎么寫還是怎么寫,只不過不再需要 textView.setText(user.name),而是直接 user.setName()路翻。
所以 DataBinding 的本質(zhì)就是 終態(tài)數(shù)據(jù) 與 UI控件 的綁定狈癞,具有以下優(yōu)勢:
- 無需多處調(diào)用控件,原本調(diào)用的地方只需要set數(shù)據(jù)即可
- 1的延伸茂契,無需手動(dòng)判空
- 1的延伸蝶桶,完全不用寫模板代碼 findViewById
- 并且,引入DataBinding后掉冶,原本的 UI 邏輯無需改動(dòng)真竖,只需設(shè)置終態(tài)數(shù)據(jù)
上篇提到過 Jetpack MVVM 架構(gòu)本質(zhì)是數(shù)據(jù)驅(qū)動(dòng),這就是說厌小,控件的狀態(tài)及數(shù)據(jù)是 被分離到 ViewModel 中管理恢共,并且 ViewModel 這一層只需負(fù)責(zé)狀態(tài)數(shù)據(jù)本身的變化,至于該數(shù)據(jù)在布局中是 被哪些視圖綁定璧亚、有沒有視圖來綁定旁振、以及怎么綁定,ViewModel 是不用關(guān)心的涨岁。
那控件是如何做到被通知且更新狀態(tài)的呢拐袜?
DataBinding 是通過 觀察者模式 來管理控件刷新狀態(tài)。當(dāng)狀態(tài)數(shù)據(jù)變化時(shí)梢薪,只需手動(dòng)地完成 setValue蹬铺,這將通知 DataBinding 去刷新 該數(shù)據(jù) 綁定的控件。
而秉撇,文章開頭提到的把邏輯放入xml中的寫法甜攀,是不建議的。數(shù)據(jù)值應(yīng) 直接反映UI控件需要的結(jié)果琐馆,而不是作為邏輯條件放在 xml 中规阀。所以,DataBinding 不負(fù)責(zé) UI 邏輯瘦麸,邏輯原本在哪里寫谁撼,現(xiàn)在還是在哪里寫,只不過滋饲,原本調(diào)用控件實(shí)例去刷新狀態(tài)的方式厉碟,現(xiàn)在改成了數(shù)據(jù)驅(qū)動(dòng)。 這就是DataBinding 的核心目標(biāo)屠缭。
1.2 例子 - 綁定列表數(shù)據(jù)
來舉個(gè)例子進(jìn)行說明:在頁面中展示用戶信息(User)列表箍鼓,同時(shí)還有兩個(gè)按鈕用于添加、移除用戶:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="clickPresenter"
type="com.hfy.demo01.module.jetpack.databinding.ListActivity.ClickPresenter" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".module.jetpack.databinding.ListActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加user"
android:onClick="@{clickPresenter::addUser}"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="移除user"
android:onClick="@{clickPresenter::removeUser}"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_user_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</layout>
我們知道呵曹,RecyclerView的所展示的列表數(shù)據(jù)款咖, 是通過Adapter 對每一項(xiàng)數(shù)據(jù) 分別進(jìn)行設(shè)置的何暮,也就是說User是綁定到 Item的xml中:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="user"
type="com.hfy.demo01.module.jetpack.databinding.bean.User" />
</data>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.name}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.level}"/>
</LinearLayout>
</layout>
我們看下,在Activity中是如何處理的:
public class ListActivity extends AppCompatActivity {
private ActivityListBinding mViewDataBinding;
private static UserListAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_list);
mViewDataBinding.rvUserList.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false));
mAdapter = new UserListAdapter();
mAdapter.setNewInstance(getUserList());
mViewDataBinding.rvUserList.setAdapter(mAdapter);
mViewDataBinding.setClickPresenter(new ClickPresenter());
}
//這里是假裝 調(diào)用ViewModel能力 獲取用戶數(shù)據(jù)
private List<User> getUserList() {
List<User> list = new ArrayList<>();
list.add(new User("小明","Lv1"));
list.add(new User("小紅","Lv2"));
list.add(new User("小q","Lv3"));
list.add(new User("小a","Lv4"));
return list;
}
//點(diǎn)擊監(jiān)聽處理
public class ClickPresenter {
public void addUser(View view) {
Toast.makeText(ListActivity.this, "addUser", Toast.LENGTH_SHORT).show();
mAdapter.addData(new User("小z","Lv5"));
}
public void removeUser(View view) {
Toast.makeText(ListActivity.this, "removeUser", Toast.LENGTH_SHORT).show();
mAdapter.remove(0);
}
}
private static class UserListAdapter extends BaseQuickAdapter<User, UserItemViewHolder> {
public UserListAdapter() {
super(R.layout.item_user);
}
@Override
protected void convert(@NonNull UserItemViewHolder holder, User user) {
// 精髓所在1铐殃,不需要去一個(gè)個(gè)setText等等
holder.getItemUserBinding().setUser(user);
holder.getItemUserBinding().executePendingBindings();
//當(dāng)獲取的DataBinding不是具體類時(shí)海洼,只是ViewDataBinding,那就要使用setVariable了
// holder.getViewDataBinding().setVariable(BR.user, user);
// holder.getViewDataBinding().executePendingBindings();
}
}
private static class UserItemViewHolder extends BaseViewHolder {
// 精髓所在2背稼,只需要持有 binding即可贰军,不用去findViewById
private final ItemUserBinding binding;
// private final ViewDataBinding binding2;
public UserItemViewHolder(View view) {
super(view);
binding = DataBindingUtil.bind(view);
// binding2 = DataBindingUtil.bind(view);
}
public ItemUserBinding getItemUserBinding() {
return binding;
}
// public ViewDataBinding getViewDataBinding() {
// return binding2;
// }
}
}
RecyclerView的初始化、調(diào)用ViewModel對數(shù)據(jù)的獲取蟹肘,這些處理及邏輯 和之前一毛一樣词疼,不同點(diǎn)在于 Item數(shù)據(jù)的展示:
- 在UserItemViewHolder中,不用去findViewById了帘腹,而是直接DataBindingUtil.bind(view)贰盗,ViewHolder只要Hold住 binding就可以了,之前是Hold住所有的view阳欲。
- 在UserListAdapter中舵盈,設(shè)置數(shù)據(jù)是,也只是使用 binding 去 setUser(user)即可球化。
二秽晚、自定義屬性 - BindingAdapter
DataBinding 還有個(gè)強(qiáng)大功能:能為控件提供自定義屬性的 BindingAdapter!
不懂筒愚?我們來看個(gè)例子赴蝇。
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:imageUrl="@{user.avatar}"
app:placeHolder="@{@drawable/dog}"/>
其中的 app:imageUrl 、app:placeHolder 分別與 user.avatar巢掺、@drawable/dog 綁定了句伶。 但我們知道ImageView本身是沒有這兩個(gè)屬性的,并且我們也并不是 繼承 ImageView 的自定義View陆淀,那為啥可以這樣使用呢考余? 再來看:
@BindingAdapter({"app:imageUrl", "app:placeHolder"})
public static void loadImageFromUri(ImageView imageView, String imageUri, Drawable placeHolder){
Glide.with(imageView.getContext())
.load(imageUri)
.placeholder(placeHolder)
.into(imageView);
}
在隨便某個(gè)類中添加 public static 方法(方法名隨意),增加注解@BindingAdapter轧苫,并且注明對應(yīng)的"app:imageUrl", "app:placeHolder"楚堤,然后方法參數(shù)是 控件類型 及 這兩個(gè)屬性對應(yīng) 值。 然后在方法中寫邏輯即可浸剩,這里就是使用Glide加載用戶頭像钾军,其中placeHolder是占位圖。
這樣就完成了 圖片的加載了绢要!
使用確實(shí)相當(dāng)簡潔,相當(dāng)于 直接自定義屬性拗小。你可以自定義 任何你想要的屬性重罪。
通常我們可以用 @BindingAdapter 方式,在模塊 內(nèi)部 來做一些公用邏輯。例如這個(gè)圖片加載剿配,@BindingAdapter注解的方法 只要寫一次搅幅,那么 所有用到 ImageView 加載圖片的地方 xml中都可以 直接使用屬性 app:imageUrl 、app:placeHolder 直接綁定數(shù)據(jù) 呼胚。
三茄唐、結(jié)合 LiveData
DataBinding 還有個(gè)妙處在于: 可以結(jié)合 LiveData 使用。
原本我們使用DataBinding蝇更,在xml中定義的variable數(shù)據(jù) 沪编,必須要繼承BaseObservable 或者使用 ObservableField,還要添加 注解 @Bindable年扩、調(diào)用notifyPropertyChanged(BR.name)蚁廓。這是為了 user.setName(name) 字段發(fā)生變化時(shí) 通知 對應(yīng)綁定View 也進(jìn)行刷新。
而 我們 上一篇 中 MVVM 是使用 LiveData厨幻,實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)的相嵌,它包裹的 User 是沒有繼承BaseObservable的, 要繼承嘛况脆? 不用饭宾!
LiveData 的出現(xiàn),就可以代替 ObservableField 格了,并且 還自動(dòng)具備 生命周期管理看铆。
不用侵入式的修改數(shù)據(jù)實(shí)體類了,直接使用LiveData笆搓,同樣支持DataBinding的數(shù)據(jù)綁定性湿!
DataBinding 結(jié)合 LiveData 使用步驟很簡單:
- 要使用LiveData對象作為數(shù)據(jù)綁定來源,需要設(shè)置LifecycleOwner
- xml中 定義變量 ViewModel满败, 并使用 ViewModel 中的 LiveData 綁定對應(yīng)控件
- binding設(shè)置變量ViewModel
//結(jié)合DataBinding使用的ViewModel
//1. 要使用LiveData對象作為數(shù)據(jù)綁定來源肤频,需要設(shè)置LifecycleOwner
binding.setLifecycleOwner(this);
ViewModelProvider viewModelProvider = new ViewModelProvider(this);
mUserViewModel = viewModelProvider.get(UserViewModel.class);
//3. 設(shè)置變量ViewModel
binding.setVm(mUserViewModel);
<!-- 2. 定義ViewModel 并綁定-->
<variable
name="vm"
type="com.hfy.demo01.module.jetpack.databinding.UserViewModel" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.userLiveData.name}"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.userLiveData.level}"/>
這樣就ok了,你會(huì)發(fā)現(xiàn) 我們不需要在 Activity中 拿到LivaData 去 observe(owner算墨,observer)了宵荒,DataBinding 自動(dòng)生成的代碼,會(huì)幫我們?nèi)プ鲞@操作净嘀,所以需要設(shè)置LifecycleOwner报咳。
也就是說,在上一篇中介紹的 Jetpack MVVM 中挖藏,如果要使用 DataBinding 的話暑刃,也是很簡單的。
四膜眠、Jetpack MVVM 補(bǔ)充說明
講完DataBinding岩臣,所有的 Jetpack 架構(gòu)組件 的重點(diǎn)內(nèi)容 就全部講完了溜嗜。
這里對 Jetpack AAC 及 MVVM ,做一些 補(bǔ)充 和 說明:
- 一架谎、ViewModel 和 View 職責(zé)分離炸宵,ViewModel中處理業(yè)務(wù)邏輯,View 僅展示數(shù)據(jù)及傳遞事件
- 二谷扣、ViewModel 不引用 View 及 Context
- 三土全、View 通過 LiveData 觀察數(shù)據(jù)變化,不是直接向View 推送數(shù)據(jù)
- 四会涎、ViewModel中 除了 業(yè)務(wù) LiveData 外裹匙,還應(yīng)該提供 LiveData<DataState>,表示數(shù)據(jù) 是加載中在塔、加載成功幻件、加載失敗。
- 五蛔溃、使用SingleLiveEvent 來傳遞 事件類消息:僅在顯式調(diào)用setValue()或call()時(shí) 才會(huì)通知觀察者绰沥;只有一個(gè)觀察者會(huì)收到更改通知。
- 六贺待、ViewModel 和 Repository 之間徽曲,建議 使用 LiveData 進(jìn)行通信,就像 View 和 ViewModel 之間那樣 使用回調(diào)的話麸塞,可能會(huì)有內(nèi)存泄漏的風(fēng)險(xiǎn)秃臣。 并且在ViewModel中 使用 Transformations.switchMap 把 生命周期信息 傳遞到 Repository 的 LiveData 中。
- 七哪工、DataBinding中綁定的數(shù)據(jù) 直接使用 LivaData 即可奥此, 而不是 BaseObservable
- 八、xml中盡量只定義一個(gè)variable雁比,那就是 頁面對應(yīng)的 ViewModel 稚虎,控件直接綁定 LivaData 的字段
- 九、XML 中盡量 不使用邏輯表達(dá)式偎捎,把邏輯放在 ViewModel 中蠢终,控件綁定終態(tài)數(shù)據(jù)
五、總結(jié)
本篇 重點(diǎn)講了 DataBinding 的重新認(rèn)知:DataBinding的本質(zhì) " 終態(tài)數(shù)據(jù) 綁定到 View " 茴她,而不是 ” 在xml寫邏輯 ”寻拂;自定義屬性 BindingAdapter;結(jié)合 LiveData的使用丈牢〖蓝ぃ可見DataBinding 在 Jetpack MVVM 架構(gòu)中 還是 有很大優(yōu)勢的。 最后補(bǔ)充說明得了 Jetpack MVVM 架構(gòu) 的使用注意事項(xiàng)和原則己沛,在實(shí)際項(xiàng)目使用中 應(yīng)該會(huì)很有體會(huì)朴皆。
到這里呢帕识,整個(gè)Jetpack AAC系列 也就結(jié)束了泛粹,到這里是第五篇了遂铡。每篇文章都想著盡可能把內(nèi)容 給介紹清楚,包括很多自己使用過后的理解晶姊。過程中也閱讀了大量 相關(guān)優(yōu)秀的文章 扒接,學(xué)習(xí)到了不同的觀點(diǎn)。雖然整個(gè)系列是經(jīng)過 閱讀源碼们衙、實(shí)際使用钾怔、閱讀其他優(yōu)秀文章 之后輸出的,但不免出現(xiàn)錯(cuò)誤和遺漏蒙挑,歡迎大家 留言討論宗侦。
如果覺得文章還不錯(cuò),想第一時(shí)間收到文章推送忆蚀,歡迎關(guān)注我的 公.眾.號(hào) 胡飛洋 矾利。如果有問題或者想進(jìn)群,號(hào)內(nèi)有加我微信的入口馋袜,我可以拉你入群男旗。在技術(shù)學(xué)習(xí)的道路上,我們一起前進(jìn)欣鳖!
參考與感謝:
ViewModel 和 LiveData:為設(shè)計(jì)模式打 Call 還是唱反調(diào)察皇?
重學(xué)安卓:從 被誤解 到 真香 的 Jetpack DataBinding!
.
你的 點(diǎn)贊泽台、評論什荣,是對我的巨大鼓勵(lì)!