本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
前言
聽說這種【一行代碼實(shí)現(xiàn)xxx】用爛的標(biāo)題總是能夠吸引到更多的關(guān)注。
在批判筆者這種行為之前,我們先來總結(jié)一下目前Android開發(fā)中通過RecyclerView列表的幾種常見實(shí)現(xiàn)方式。
- 1.直接使用原生RecyclerView提供的API,自己實(shí)現(xiàn)RecyclerView的Adapter和ViewHolder。
- 2.使用網(wǎng)上比較火的三方庫(kù),類似一行代碼實(shí)現(xiàn)上拉加載更多,下拉刷新,xxx,xxx的RecyclerViewAdapter;或者個(gè)人開發(fā)者基于此類,再度封裝的BaseAdapter。
- 3.使用Databinding,寫一個(gè)一勞永逸的Adapter,從此告別Adapter的多次實(shí)現(xiàn)。
筆者闡述一下個(gè)人對(duì)于上述3種列表的實(shí)現(xiàn)方式的評(píng)價(jià):
1.直接使用原生RecyclerView提供的API,自己實(shí)現(xiàn)RecyclerView的Adapter和ViewHolder。
簡(jiǎn)單而又直接,無(wú)論是列表的實(shí)現(xiàn)者,還是后來代碼的維護(hù)者,都能第一時(shí)間理解代碼的意圖,但是弊端很明顯,那就是Adapter類和ViewHolder類過于繁多,每一個(gè)列表都需要一個(gè)對(duì)應(yīng)的Adapter和ViewHolder。
對(duì)于偷懶的程序員來講,這種重復(fù)性的行為顯然是難以令人接受的。
2.使用網(wǎng)上比較火的三方庫(kù),類似一行代碼實(shí)現(xiàn)上拉加載更多,下拉刷新,xxx,xxx的RecyclerViewAdapter;或者個(gè)人開發(fā)者基于此類,再度封裝的BaseAdapter。
事實(shí)上,現(xiàn)在網(wǎng)絡(luò)上越來越多出現(xiàn)別人封裝好的RecyclerViewAdapter或其他工具,恕筆者直言,大多數(shù)都略有嘩眾取寵之嫌,很多都不可避免出現(xiàn)了 過度封裝 的情況:它也許能夠涵括大多數(shù)的需求,但是這也恰恰是它致命的弊端,在涉及一些新的功能時(shí),它也許會(huì)突然無(wú)能為力——過度的封裝帶來了嚴(yán)重的耦合,這種問題是架構(gòu)級(jí)的。
一個(gè)良好的設(shè)計(jì)需要更多的思考和嘗試,更重要的也許是靈活,高度的可拓展性。
在這里筆者推薦一個(gè)已經(jīng)使用了很久的庫(kù):drakeet大神 的 【MultiType】:An Android library to create multiple item types list views easily and flexibly
3.使用Databinding,寫一個(gè)一勞永逸的Adapter,從此告別Adapter的多次實(shí)現(xiàn)
DataBinding,google推出的大名鼎鼎的庫(kù),也是Android開發(fā)中MVVM架構(gòu)中不可或缺的基礎(chǔ)組件之一,它的定義很純粹,那就是
數(shù)據(jù)驅(qū)動(dòng)視圖
很遺憾的是,因?yàn)镸VP模式的便利和簡(jiǎn)單(是簡(jiǎn)單而不是簡(jiǎn)潔,事實(shí)上,MVP開發(fā)模式的強(qiáng)大也是掣肘它的原因之一,一個(gè)單純的界面至少需要Contract+MVP多個(gè)文件進(jìn)行配置,還不算dagger2Component+Module文件的配置,隨著開發(fā)時(shí)間的延長(zhǎng),這種疑問逐漸在我腦海中浮現(xiàn)),MVP的擁護(hù)者確實(shí)比MVVM多一些,DataBinding并未大面積在Android開發(fā)中拓展開來,也是因此,筆者也很少看到關(guān)于DataBinding的實(shí)踐和總結(jié)的文章。
通過DataBinding實(shí)現(xiàn)列表,這種需求的實(shí)現(xiàn)已經(jīng)不是什么難事,網(wǎng)上一搜文章一大堆,但是仍然略微有些復(fù)雜。
筆者今天嘗試將個(gè)人開發(fā)過程中,對(duì)通過DataBinding實(shí)現(xiàn)RecyclerView列表的方式所得進(jìn)行一次簡(jiǎn)單的分享,不足之處,歡迎拍磚。
注意,本文需要讀者有一定的DataBinding的使用基礎(chǔ)。
DataBinding簡(jiǎn)介
如果您對(duì)于DataBinding并不是很熟悉,不用擔(dān)心,接下來我將盡量用簡(jiǎn)潔的語(yǔ)言對(duì)DataBinding進(jìn)行簡(jiǎn)單的介紹,讓客官最快了解DataBinding的基本語(yǔ)法和其優(yōu)勢(shì)。
如果您已經(jīng)對(duì)DataBinding有過一定的學(xué)習(xí),可跳過本小節(jié),直接閱讀[正文]部分。
1.DataBinding是什么
Data Binding Library (數(shù)據(jù)綁定庫(kù)),旨在減少綁定應(yīng)用程序邏輯和布局所需的一些耦合性代碼。
DataBinding最低支持Android 2.1 (API Level 7)。
2.添加DataBinding的依賴
在build.gradle添加如下聲明,添加DataBinding的依賴:
android {
....
dataBinding {
enabled = true
}
}
3.在你的布局文件中綁定數(shù)據(jù)
我們將activity_main.xml的布局文件進(jìn)行這樣的配置:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.example.User"/> //綁定一個(gè)User類型的數(shù)據(jù)
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}"/> //TextView,顯示的內(nèi)容為user.firstName
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}"/> //TextView,顯示的內(nèi)容為user.lastName
</LinearLayout>
</layout>
以上文demo為例,在<data>標(biāo)簽中 <variable>描述了一個(gè)變量user,它對(duì)應(yīng)的類型為”com.example.User”
布局中使用“@ { }”語(yǔ)法來書寫表達(dá)式。如為TextView的文本設(shè)置成user. firstName。
這看起來就好像是,我們將一個(gè)user對(duì)象,傳給了xml布局文件,布局文件中的控件根據(jù)這個(gè)對(duì)象的對(duì)應(yīng)屬性,顯示對(duì)應(yīng)的數(shù)據(jù)。
順便提供一下User類:
public class User {
public String firstName;
public String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
4.如何綁定數(shù)據(jù)
從3的代碼中,衍生出一個(gè)問題,我們?nèi)绾螌ser對(duì)象傳給activity_main的布局文件呢?
我們來看代碼,我們?cè)贛ainActivity中進(jìn)行這樣的配置,以代替Activity傳統(tǒng)的setContentView(layoutRes)方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//MainActivity綁定activity_main布局,類似setContentView()方法
MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
//初始化User并賦值給Binding(你也可以先暫時(shí)理解為賦值給xml布局文件)
User user = new User("Test", "User");
binding.setUser(user);
}
好了,配置到這里,我們就可以運(yùn)行demo,然后觀察到,MainActivity界面的兩個(gè)TextView,都成功顯示了User的對(duì)應(yīng)屬性了!
這種通過數(shù)據(jù)綁定,從而控制控件顯示的方式,相比傳統(tǒng)的TextView.setText(user.name),好處在哪呢,最直觀的好處是:
- 避免Activity中多次使用findViewById,損害了應(yīng)用性能且令人厭煩。
- 傳統(tǒng)的方式,更新UI數(shù)據(jù)需切換至UI線程,并將數(shù)據(jù)分解映射到各個(gè)view(比如TextView.setText()),而DataBinding避免了這種麻煩的行為。
5.DataBinding的更多支持
除了上文的說到功能,DataBinding還提供了更多優(yōu)秀的特性支持,對(duì)此請(qǐng)參考官方文檔說明(本小節(jié)的示例代碼也是節(jié)選自官方文檔),篇幅所限,無(wú)法一一舉例,還望諒解。
https://developer.android.google.cn/topic/libraries/data-binding/index.html
看到這里,雖然你可能還是對(duì)DataBinding一頭霧水——僅僅是看懂了最基本的使用方式,而沒有理解到DataBinding絕對(duì)的優(yōu)勢(shì)在哪里。
沒有關(guān)系,來看一看,筆者在目前項(xiàng)目中,通過DataBinding對(duì)RecyclerView的一種“新的”實(shí)現(xiàn)方式,相信有了剛才的趁熱打鐵,即使您沒有使用過DataBinding,對(duì)接下來的代碼也能夠大概看明白。
這之后,如果您對(duì)DataBinding感興趣,再嘗試花時(shí)間去慢慢研究和使用它。
正文
如何一行Java代碼都不要,實(shí)現(xiàn)RecyclerView列表呢?
真的不需要Java代碼就能實(shí)現(xiàn)列表嗎?
對(duì)不起朋友們,我騙了你們,是需要代碼的。
打死標(biāo)題黨!
請(qǐng)?jiān)谀鷵]舞下手中40米長(zhǎng)的大刀之前,來看一下筆者實(shí)現(xiàn)列表的代碼:
單類型列表的實(shí)現(xiàn)
先看下MainActivity的java代碼
public class MainActivity extends AppCompatActivity {
//要展示的數(shù)據(jù)源
public final ObservableArrayList<Student> showDatas = new ObservableArrayList<>();
{
//初始化數(shù)據(jù)源
for (int i = 0; i < 20; i++) {
students.add(new Student("學(xué)生:" + i));
}
showDatas.addAll(students);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//完成數(shù)據(jù)和布局的綁定
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setActivity(this);
}
public void onBindItem(ViewDataBinding binding, Object data, int position) {
binding.getRoot().setOnClickListener(v -> Toast.makeText(this, data.toString(), Toast.LENGTH_SHORT).show());
}
//數(shù)據(jù)的實(shí)體類
public class Student {
public String name;
public Student(String name) {
this.name = name;
}
}
}
筆者保證,除了MainActivity.java類外,不再有任何MainActivity相關(guān)的Java文件(比如MainPresenter ,MainModel , MainActivityListAdapter等)。
運(yùn)行App,讓我們來看一下MainActivity的UI:

現(xiàn)在我們點(diǎn)擊單個(gè)Item,Item還會(huì)響應(yīng)對(duì)應(yīng)的點(diǎn)擊事件——彈出一個(gè)toast,并打印對(duì)應(yīng)的Student對(duì)象。
熟悉DataBinding的朋友們肯定有一些猜測(cè)了,我們來看一下對(duì)應(yīng)的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="activity"
type="com.qingmei2.simplerecyclerview.MainActivity" />
</data>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{activity.showDatas}"
app:layoutManager="@string/linear_layout_manager"
app:itemLayout="@{@layout/item_student_list}"
app:onBindItem="@{activity::onBindItem}" />
</layout>
以及對(duì)應(yīng)的item的layout.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="data"
type="com.qingmei2.simplerecyclerview.Student" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="vertical">
<!--顯示人名的TextView-->
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:padding="8dp"
android:text="@{data.name}"
tools:text="小明" />
<!--Item下方灰色的分割線-->
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#ccc" />
</LinearLayout>
</layout>
不可否認(rèn)的是,作為MainActivity的一個(gè)列表,筆者確實(shí)沒有使用Java代碼實(shí)現(xiàn)RecyclerView的Adapter和ViewHolder,以及設(shè)置LayoutManager,哪怕一行都沒有。
我們先來看一下魔法的根源,即activity_main.xml文件的RecyclerView的配置,一切的實(shí)現(xiàn)都來源于下面的四條屬性:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{activity.showDatas}" //要顯示的數(shù)據(jù)源
app:layoutManager="@string/linear_layout_manager" //指定LayoutManager
app:itemLayout="@{@layout/item_student_list}" //數(shù)據(jù)展示在哪個(gè)布局上
app:onBindItem="@{activity::onBindItem}" /> //更多配置,比如我想設(shè)置點(diǎn)擊事件,或者引用Context
我們拋開怎么實(shí)現(xiàn),先闡述這四條屬性,為何就能展示一個(gè)完整的列表呢?
//1.要顯示的數(shù)據(jù)源
app:items="@{activity.showDatas}"
我們從MainActivity中可以看到,activity.showDatas實(shí)際上就是ObservableArrayList<Student>類型的List, ObservableArrayList本身就是ArrayList的子類,這個(gè)屬性的意義在于,告訴RecyclerView:
你需要展示的列表所需要的數(shù)據(jù)都在這里了,這個(gè)List有多少條數(shù)據(jù),你就展示多少個(gè)item。
顯然,我們?cè)诖a中,通過模擬網(wǎng)絡(luò)請(qǐng)求的結(jié)果,給list初始化了20條Student數(shù)據(jù),因此,RecyclerView知道,需要展示20條數(shù)據(jù),并為其創(chuàng)建20條item展示出來。
那么數(shù)據(jù)有了,問題來了,數(shù)據(jù)如何展示給用戶呢?
因此我們需要配置item對(duì)應(yīng)的layout文件:
//2.數(shù)據(jù)展示在哪個(gè)布局上
app:itemLayout="@{@layout/item_student_list}"
我們將item_student_list.xml——item的布局文件傳給了RecyclerView,RecyclerView就知道了如何將數(shù)據(jù)展示在item上。
那么,數(shù)據(jù)如何展示在item上的呢?請(qǐng)往上翻,我們可以看到,item的layout文件中,也已經(jīng)將我們要展示的Student作為data傳進(jìn)了item的LayoutBinding中,layout的子控件就會(huì)知道,該如何展示student的數(shù)據(jù)了。比如,將student的name展示在TextView上:
<!--顯示人名的TextView-->
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:padding="8dp"
android:text="@{data.name}"
tools:text="小明" />
現(xiàn)在數(shù)據(jù)和布局都有了,RecyclerView還需要知道,如何布局?是LinearLayout還是GridLayout呢?
很簡(jiǎn)單,我們傳進(jìn)來就可以了:
//3.指定LayoutManager
app:layoutManager="@string/linear_layout_manager"
簡(jiǎn)單明了,我們指定使用了LinearLayoutManager
其實(shí)按理說,上述3條屬性已經(jīng)夠用了,但是我們還需要考慮到一些拓展的需求,比如點(diǎn)擊事件,或者和Activity/Fragment的聯(lián)動(dòng)?
//4 更多配置的回調(diào)
app:onBindItem="@{activity::onBindItem}"
我們聲明了一個(gè)回調(diào),并在MainActivity中實(shí)現(xiàn)了這個(gè)回調(diào):
public void onBindItem(ViewDataBinding binding, Object data, int position) {
binding.getRoot().setOnClickListener(v -> Toast.makeText(this, data.toString(), Toast.LENGTH_SHORT).show());
}
demo中很簡(jiǎn)單,我們只聲明了一個(gè)點(diǎn)擊事件。事實(shí)上,也許有更多的需求,比如根據(jù)item中控件狀態(tài)的變更(比如checkbox等),來做出對(duì)應(yīng)的行為,我們?cè)诨卣{(diào)中聲明了3個(gè)參數(shù):
- ViewDataBinding binding:item的Binding,通過向下轉(zhuǎn)型即可獲得對(duì)應(yīng)的Binding對(duì)象,比如本文的ItemStudentListBinding
- Object data : item對(duì)應(yīng)的數(shù)據(jù),通過向下轉(zhuǎn)型即可獲得對(duì)應(yīng)的對(duì)象,比如本文中可以轉(zhuǎn)換為Student
- int position:很明顯,就是item在list中的索引
示例代碼:
public void onBindItem(ViewDataBinding binding, Object data, int position) {
ItemStudentListBinding bind = (ItemStudentListBinding) binding;
Student student = (Student) data;
//點(diǎn)擊item,toast,打印學(xué)生的name
bind .getRoot().setOnClickListener(v -> Toast.makeText(this, student.name, Toast.LENGTH_SHORT).show());
}
看起來很像RecyclerView的Adapter的onBindViewHolder方法?原理也確實(shí)如此,只不過是將這個(gè)接口暴漏出來,方便開發(fā)者進(jìn)行特殊處理。
相信,這四個(gè)屬性的提供,足以實(shí)現(xiàn)各種各樣 單類型列表 的需求了。
成功男人背后的女人
如果您的項(xiàng)目中使用了DataBinding,從此之后,您項(xiàng)目中的RecyclerView都將是這么的簡(jiǎn)潔:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{activity.showDatas}"
app:layoutManager="@string/linear_layout_manager"
app:itemLayout="@{@layout/item_student_list}"
app:onBindItem="@{activity::onBindItem}" />
在此之前,您需要進(jìn)行一些配置,這些配置我已經(jīng)連同本文的Demo一起放在了我的github上,供您參考:
請(qǐng)先將目光轉(zhuǎn)回到本文中,我們一起實(shí)現(xiàn)幾個(gè)簡(jiǎn)單的類:
1.添加MultiType的依賴
正如前言所說, MultiType是一個(gè)靈活且可以高度拓展的庫(kù),本文的demo也是基于其進(jìn)行的開發(fā):
implementation 'me.drakeet.multitype:multitype:3.3.0'
implementation 'com.annimon:stream:1.1.9'
同時(shí),為了代碼簡(jiǎn)潔,我添加了Java8的StreamAPI的向下兼容庫(kù)的依賴,您也可以選擇不添加,只需要將對(duì)應(yīng)的Java8方法轉(zhuǎn)換為普通的方法即可,而不會(huì)影響對(duì)應(yīng)的功能。
當(dāng)然,我們不要忘記在android的目錄下添加databinding的支持:
android {
dataBinding {
enabled = true
}
}
2.實(shí)現(xiàn)DataBindingItemViewBinder和DataBindingViewHolder
public class DataBindingItemViewBinder<T, DB extends ViewDataBinding>
extends ItemViewBinder<T, DataBindingViewHolder<DB>> {
private final Delegate<T, DB> delegate;
public DataBindingItemViewBinder(Delegate<T, DB> delegate) {
this.delegate = delegate;
}
public DataBindingItemViewBinder(BiFunction<LayoutInflater, ViewGroup, DB> factory,
OnBindItem<T, DB> binder) {
this(new SimpleDelegate<>(factory, binder));
}
public DataBindingItemViewBinder(@LayoutRes int resId, OnBindItem<T, DB> binder) {
this((inflater, parent) -> DataBindingUtil.inflate(inflater, resId, parent, false), binder);
}
@NonNull
@Override
protected DataBindingViewHolder<DB> onCreateViewHolder(@NonNull LayoutInflater inflater,
@NonNull ViewGroup parent) {
return new DataBindingViewHolder<>(delegate.onCreateDataBinding(inflater, parent));
}
@Override
protected void onBindViewHolder(@NonNull DataBindingViewHolder<DB> holder, @NonNull T item) {
final DB binding = holder.dataBinding;
binding.setVariable(BR.data, item);//數(shù)據(jù)綁定對(duì)應(yīng)的item layout
delegate.onBind(binding, item, holder.getAdapterPosition());//回調(diào)
binding.executePendingBindings();
}
public interface Delegate<T, DB extends ViewDataBinding> {
DB onCreateDataBinding(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);
void onBind(@NonNull DB dataBinding, @NonNull T item, int position);
}
public interface OnBindItem<T, DB extends ViewDataBinding> {
void bind(DB dataBinding, T data, int position);
}
private static class SimpleDelegate<T, DB extends ViewDataBinding> implements Delegate<T, DB> {
private final BiFunction<LayoutInflater, ViewGroup, DB> factory;
private final OnBindItem<T, DB> binder;
SimpleDelegate(BiFunction<LayoutInflater, ViewGroup, DB> factory, OnBindItem<T, DB> binder) {
this.factory = factory;
this.binder = binder;
}
@Override
public DB onCreateDataBinding(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
return factory.apply(inflater, parent);
}
@Override
public void onBind(@NonNull DB dataBinding, @NonNull T item, int position) {
binder.bind(dataBinding, item, position);
}
}
}
public class DataBindingViewHolder<T extends ViewDataBinding> extends RecyclerView.ViewHolder {
public final T dataBinding;
public DataBindingViewHolder(T binding) {
super(binding.getRoot());
dataBinding = binding;
}
}
這兩個(gè)類的作用就是通過代理的方式實(shí)現(xiàn)了通用的Adapter和ViewHolder,我們實(shí)現(xiàn)了它們,只要不是過于復(fù)雜的列表,我們都不再需要實(shí)現(xiàn)RecyclerView的Adapter和ViewHolder了。
我將不會(huì)對(duì)這兩個(gè)核心類有過多的講解,因?yàn)樗鼈儗?duì)于熟悉Databinding的使用者來說,并不難以理解。
如果您對(duì)于DataBinding并不是很熟悉,筆者建議您暫時(shí)先新建這兩個(gè)類,并將代碼復(fù)制上去——當(dāng)您能夠駕輕就熟地使用這個(gè)工具后,再嘗試研究它的原理,相信我,它的原理本身也并不復(fù)雜。
3.實(shí)現(xiàn)對(duì)應(yīng)的BindingAdapter和Linker類
public class RecyclerViewBindingAdapter {
public static class BindableVariables extends BaseObservable {
@Bindable
public Object data;
}
@BindingAdapter({"itemLayout", "onBindItem"})
public static void setAdapter(RecyclerView view, int resId, DataBindingItemViewBinder.OnBindItem onBindItem) {
final MultiTypeAdapter adapter = getOrCreateAdapter(view);
//noinspection unchecked
adapter.register(Object.class, new DataBindingItemViewBinder(resId, onBindItem));
}
private static MultiTypeAdapter getOrCreateAdapter(RecyclerView view) {
if (view.getAdapter() instanceof MultiTypeAdapter) {
return (MultiTypeAdapter) view.getAdapter();
} else {
final MultiTypeAdapter adapter = new MultiTypeAdapter();
view.setAdapter(adapter);
return adapter;
}
}
@BindingAdapter({"linkers", "onBindItem"})
public static void setAdapter(RecyclerView view, List<Linker> linkers, DataBindingItemViewBinder.OnBindItem onBindItem) {
final MultiTypeAdapter adapter = getOrCreateAdapter(view);
//noinspection unchecked
final ItemViewBinder[] binders = Stream.of(linkers)
.map(Linker::getLayoutId)
.map(v -> new DataBindingItemViewBinder(v, onBindItem))
.toArray(ItemViewBinder[]::new);
//noinspection unchecked
adapter.register(Object.class)
.to(binders)
.withLinker(o -> Stream.of(linkers)
.map(Linker::getMatcher)
.indexed()
.filter(v -> v.getSecond().apply(o))
.findFirst()
.map(IntPair::getFirst)
.orElse(0));
}
@BindingAdapter("items")
public static void setItems(RecyclerView view, List items) {
final MultiTypeAdapter adapter = getOrCreateAdapter(view);
adapter.setItems(items == null ? Collections.emptyList() : items);
adapter.notifyDataSetChanged();
}
}
public class Linker {
private final Function<Object, Boolean> matcher;
private final int layoutId;
public static Linker of(Function<Object, Boolean> matcher, int layoutId) {
return new Linker(matcher, layoutId);
}
public Linker(Function<Object, Boolean> matcher, int layoutId) {
this.matcher = matcher;
this.layoutId = layoutId;
}
public Function<Object, Boolean> getMatcher() {
return matcher;
}
public int getLayoutId() {
return layoutId;
}
}
DataBinding提供了@BindingAdapter注解,用于綁定View和拓展對(duì)應(yīng)的行為,關(guān)于這個(gè)注解,我們通過百度或者谷歌,都能搜到大量的學(xué)習(xí)資料,在此也不多贅述。
我們可以看到,RecyclerViewBindingAdapter 這個(gè)類中,聲明了我們剛剛認(rèn)識(shí)并了解了的幾個(gè)屬性,比如“itemLayout”,“onBindItem”,“items”等屬性,聲明了這些屬性的靜態(tài)方法,作用也就是自動(dòng)創(chuàng)建對(duì)應(yīng)的Adapter,然后進(jìn)行數(shù)據(jù)與視圖的綁定。
我們可以看到除了幾個(gè)熟悉的屬性,我們還聲明了"linkers"屬性,以及聲明了一個(gè)Linker類,它們的作用是用來實(shí)現(xiàn)多類型列表,我們下文將會(huì)提到。
4.其他的配置
1 2 3 的步驟已經(jīng)將核心的類都聲明完畢,接下來我們需要在attr.xml文件中聲明我們需要用到的屬性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RecyclerView">
<attr name="items" format="reference" />
<attr name="itemLayout" format="reference" />
<attr name="linkers" format="reference" />
<attr name="layoutManager" format="reference" />
<attr name="onBindItem" format="reference" />
</declare-styleable>
</resources>
這樣,我們?cè)趚ml文件中,直接通過代碼提示的功能,為RecyclerView賦予對(duì)應(yīng)的配置了。
然后,在values.xml文件中,聲明好我們要引用的layoutManager:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="linear_layout_manager">android.support.v7.widget.LinearLayoutManager</string>
<string name="grid_layout_manager">android.support.v7.widget.GridLayoutManager</string>
</resources>
配置到這里,上面demo中我們實(shí)現(xiàn)的功能就已經(jīng)可以實(shí)現(xiàn)了,我們的這些配置類,都是一次聲明,之后項(xiàng)目中無(wú)需再進(jìn)行處理的,也就是說,隨著項(xiàng)目中列表越來越多,我們將會(huì)節(jié)省越來越多的代碼。
最后,不管再多的RecyclerView,我們都只需要配置好xml文件中RecyclerView對(duì)應(yīng)的四條屬性,然后,告別繁多的Adapter,LayoutManager和ViewHolder,and enjoy coding!
(PS,對(duì)于Activity的onBindItem的回調(diào)方法,復(fù)雜的需求也許會(huì)導(dǎo)致很臃腫,比如狀態(tài)的判斷處理,這也是一直在思考能否再簡(jiǎn)化的地方,有思路的朋友望請(qǐng)不吝賜教!)
多類型列表需要幾行代碼?
大概,也是0行吧。
一個(gè)簡(jiǎn)單的demo:

這仍然是一個(gè)RecyclerView列表,不同的是,它需要展示Teacher和Student兩種數(shù)據(jù)(因?yàn)楣P者懶,所以2種數(shù)據(jù)沒有打亂排列,但是請(qǐng)相信,他們?nèi)蕴幱谕粋€(gè)RecyclerView,并對(duì)應(yīng)不同的布局和邏輯處理)。
讓我們看一看代碼:
public class MainActivity extends AppCompatActivity {
//要展示數(shù)據(jù)源
public final ObservableArrayList<Object> showDatas = new ObservableArrayList<>();
//Linker對(duì)象的list,用來管理item展示的邏輯
public final ObservableArrayList<Linker> linkers = new ObservableArrayList<>();
public final List<Student> students = new ArrayList<>();
public final List<Teacher> teachers = new ArrayList<>();
{
for (int i = 0; i < 20; i++) {
students.add(new Student("學(xué)生:" + i));
}
for (int j = 0; j < 5; j++) {
teachers.add(new Teacher("教師:" + j, "年齡:" + (20 + j)));
}
linkers.add(
new Linker(
o -> o instanceof Student,
R.layout.item_student_list
)//如果item的數(shù)據(jù)是Student類型,使用item_student_list布局
);
linkers.add(
new Linker(
o -> o instanceof Teacher,
R.layout.item_teacher_list
)//如果item的數(shù)據(jù)是Teacher類型,使用item_teacher_list布局
);
showDatas.addAll(students);
showDatas.addAll(teachers);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setActivity(this);
}
public void onBindItem(ViewDataBinding binding, Object data, int position) {
binding.getRoot().setOnClickListener(v -> Toast.makeText(MainActivity.this, data.toString(), Toast.LENGTH_SHORT).show());
}
}
我們看到,依然沒有Adapter,ViewHolder(如果是常規(guī)實(shí)現(xiàn)方式,這里應(yīng)該是2種ViewHolder的類),以及LayoutManager。
看一下布局文件:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{activity.showDatas}"
app:layoutManager="@string/linear_layout_manager"
app:linkers="@{activity.linkers}" //請(qǐng)注意這行
app:onBindItem="@{activity::onBindItem}" />
和單類型列表相比,我們少了
app:itemLayout="@{@layout/item_student_list}"
多了
app:linkers="@{activity.linkers}"
很好理解,對(duì)于多類型列表的展示,我們會(huì)定義多個(gè)不同item的layout布局文件,因此我們不能單純的為RecyclerView賦予固定的布局,而是賦予其不同item的所有l(wèi)ayout文件
R.layout.item_student_list
R.layout.item_teacher_list
接下來需要思考的問題是,我們?nèi)绾蔚弥恳粋€(gè)item需要使用哪種類型的布局呢?
我們可以通過一個(gè)函數(shù),來判斷item數(shù)據(jù)的類型,如果是Student類型,就使用R.layout.item_student_list ,如果是Teacher類型,就使用R.layout.item_teacher_list。
因此我們衍生出了Linker類(見上文),它包含了一個(gè)LayoutRes屬性和一個(gè)Function<Object, Boolean>函數(shù),我們初始化時(shí),根據(jù)數(shù)據(jù)對(duì)應(yīng)的類型進(jìn)行判斷,如果函數(shù)的返回值為true,就使用其內(nèi)部的LayoutRes并進(jìn)行展示:
linkers.add(new Linker(
o -> o instanceof Student,
R.layout.item_student_list
)//如果item的數(shù)據(jù)是Student類型,使用item_student_list布局
);
linkers.add(new Linker(
o -> o instanceof Teacher,
R.layout.item_teacher_list
)//如果item的數(shù)據(jù)是Teacher類型,使用item_teacher_list布局
);
小結(jié)
在使用MVVM模式進(jìn)行項(xiàng)目開發(fā)的大半年里,收獲良多,在此尤其感謝同事Z0君對(duì)自己的很多指點(diǎn)(事實(shí)上,本文的實(shí)現(xiàn)完全是來源于TA的思路,筆者只不過照搬,理解和闡述分享而已),同時(shí)感謝項(xiàng)目中共同一起開發(fā)的小伙伴們,共勉。
在本文的標(biāo)題選擇上,筆者選擇了這么強(qiáng)目的性的標(biāo)題,也確實(shí)希望能夠有更多朋友能夠一起打開本文,閱讀并共同探討,希望以文章的內(nèi)容能夠表達(dá)筆者對(duì)此的歉意。
真誠(chéng)地感謝,您能夠堅(jiān)持閱讀到這里,對(duì)文章內(nèi)容的肯定,就是對(duì)作者最大的鼓勵(lì)。
本文Demo的源碼傳送門,有興趣的朋友可以拉下來運(yùn)行一下,希望能夠提供一定的思路: