前言
Android真響應(yīng)式架構(gòu)系列文章:
Android真響應(yīng)式架構(gòu)——MvRx
Epoxy——RecyclerView的絕佳助手
Android真響應(yīng)式架構(gòu)——Model層設(shè)計(jì)
Android真響應(yīng)式架構(gòu)——數(shù)據(jù)流動(dòng)性
Android真響應(yīng)式架構(gòu)——Epoxy的使用
Android真響應(yīng)式架構(gòu)——MvRx和Epoxy的結(jié)合
在第一篇文章中恳邀,我就說(shuō)過(guò)去枷,MvRx界面響應(yīng)式的關(guān)鍵在于Epoxy斥废,并且在第二篇文章中對(duì)Epoxy的使用方式做了簡(jiǎn)單介紹气忠。我個(gè)人認(rèn)為炕吸,MvRx真正難以掌握的是Epoxy覆致,而不是MvRx本身愈涩。你可以去查看一下MvRx的代碼望抽,真的沒(méi)有幾個(gè)類,也沒(méi)有多少代碼履婉,還是很容易理解的煤篙。但是Epoxy就顯得復(fù)雜多了,代碼量及復(fù)雜程度都大大增加毁腿。雖說(shuō)辑奈,MvRx將Epoxy視為可選項(xiàng),但是已烤,我覺(jué)得沒(méi)有Epoxy的話鸠窗,MvRx的作用將大打折扣。如果沒(méi)有Epoxy胯究,MvRx也就失去了界面響應(yīng)式的能力稍计,那么MvRx也不能稱之為“真響應(yīng)式架構(gòu)”,雖說(shuō)真不真的也沒(méi)有什么意義(這個(gè)名字也是我瞎起的)裕循,至少M(fèi)vRx相較于Android Architecture Component的優(yōu)勢(shì)就小了很多臣嚣。所以净刮,我還是推薦MvRx結(jié)合Epoxy一起使用的。
Epoxy之于MvRx的作用是毋庸置疑的硅则,但是淹父,Epoxy本身的復(fù)雜性也是無(wú)法回避的。關(guān)于Epoxy怎虫,我個(gè)人的理解也有限暑认,這篇文章談?wù)劊以谑褂肊poxy的過(guò)程中遇到的容易出錯(cuò)的地方大审,以及一種不太容易想到的使用方式蘸际。
這篇文章主要講兩點(diǎn):1. Epoxy是如果設(shè)置item的點(diǎn)擊事件的;2. 使用Epoxy對(duì)RecyclerView進(jìn)行嵌套使用饥努,以拓展Epoxy的使用范圍捡鱼。以上內(nèi)容都是基于Epoxy的具體實(shí)踐,會(huì)涉及到Epoxy的很多內(nèi)容酷愧,這些內(nèi)容我不可能面面俱到驾诈,希望你已經(jīng)熟悉Epoxy的基本使用方式,然后再來(lái)看這篇文章溶浴。
1. 點(diǎn)擊事件
在Epoxy——RecyclerView的絕佳助手文中乍迄,提到了如何設(shè)置點(diǎn)擊事件,講得很簡(jiǎn)單士败,實(shí)際上這是個(gè)很tricky的點(diǎn)闯两。
在Epoxy中我們經(jīng)常這么設(shè)置點(diǎn)擊事件:
@CallbackProp
fun onClickListener(listener: OnClickListener?) {
setOnClickListener(listener)
}
這實(shí)際上等價(jià)于
@ModelProp(options = {Option.NullOnRecycle, Option.DoNotHash})
fun onClickListener(listener: OnClickListener?) {
setOnClickListener(listener)
}
CallbackProp
注解相當(dāng)于ModelProp
注解設(shè)置了NullOnRecycle
和DoNotHash
兩個(gè)選項(xiàng)。NullOnRecycle
的含義是:當(dāng)View滑出屏幕谅将,從RecyclerView解綁時(shí)漾狼,將對(duì)應(yīng)的屬性設(shè)為null(對(duì)于上例而言,即調(diào)用onClickListener(null)
)饥臂;DoNotHash
的含義是:該屬性的hashcode發(fā)生變化時(shí)逊躁,不進(jìn)行重新的綁定。這兩點(diǎn)都非常符合類似于點(diǎn)擊事件這樣的回調(diào)隅熙。通常情況下稽煤,我們都會(huì)使用匿名內(nèi)部類的方式去設(shè)置點(diǎn)擊事件的回調(diào),如果沒(méi)有設(shè)置DoNotHash
囚戚,那么每次EpoxyModels重建時(shí)(調(diào)用requestModelBuild
方法)酵熙,那么所有包含點(diǎn)擊事件回調(diào)的EpoxyModel都會(huì)被認(rèn)為是發(fā)生了改變的,因?yàn)辄c(diǎn)擊事件的回調(diào)是以匿名內(nèi)部類的方式實(shí)現(xiàn)的驰坊,這次匿名內(nèi)部類的hashcode自然和上次的不同匾二,這會(huì)導(dǎo)致幾乎所有EpoxyModel都需要重新綁定到RecyclerView上。然而,一般而言察藐,這是不必要的借嗽,因?yàn)殡m然匿名內(nèi)部類不相等了,但是匿名內(nèi)部類表達(dá)的含義并沒(méi)有改變转培,因此沒(méi)必要重新綁定,而DoNotHash
正是起到這樣的作用浆竭。所以說(shuō)浸须,實(shí)際上DoNotHash
或者說(shuō)CallbackProp
起到了一定優(yōu)化的作用。
但是邦泄,這里面有個(gè)大問(wèn)題删窒,如果我們表示回調(diào)的匿名內(nèi)部類捕獲了外部的變量,當(dāng)這個(gè)回調(diào)被調(diào)用時(shí)顺囊,這個(gè)變量可能已經(jīng)過(guò)時(shí)了肌索。來(lái)看個(gè)例子:
@ModelView
class OptionItem @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {
@ModelProp
fun setName(name: CharSequence?) {
text = name
}
@ModelProp
fun setChecked(checked: Boolean) {
if (checked)
setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.checked, 0)
else
setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)
}
@CallbackProp
fun onClickListener(listener: OnClickListener?) {
setOnClickListener(listener)
}
}
//使用 OptionItem 的代碼片段
subjects.forEach { subject ->
optionItem {
id(subject.id)
name(subject.name)
//已經(jīng)選擇的科目ID為checkedSubjectID
checked(checkedSubjectID == subject.id)
onClickListener { view ->
//這里捕獲了外層變量checkedSubjectID
if (checkedSubjectID != subject.id)
//...
}
}
}
在Kotlin中我們一般使用lambda表達(dá)式來(lái)實(shí)現(xiàn)點(diǎn)擊事件的回調(diào),本質(zhì)上跟匿名內(nèi)部類是一樣的特碳。在該lambda表達(dá)式內(nèi)部诚亚,我們捕獲了外部變量checkedSubjectID
,但是該變量會(huì)隨著我們切換學(xué)科而改變午乓,當(dāng)點(diǎn)擊事件發(fā)生站宗,lambda表達(dá)式被調(diào)用時(shí),被捕獲的checkedSubjectID
的值可能已經(jīng)過(guò)時(shí)了益愈。這是因?yàn)樯颐穑覀兪褂昧?code>DoNotHash,第一次lambda表達(dá)式捕獲的變量checkedSubjectID
是多少蒸其,之后就總是那個(gè)值敏释,不會(huì)改變。這顯然是不行的摸袁,Epoxy的做法是使用OnModelClickListener
來(lái)替代OnClickListener
接口钥顽。以下是OnModelClickListener
接口的定義:
/** Used to register a click listener on a generated model. */
public interface OnModelClickListener<T extends EpoxyModel<?>, V> {
/**
* Called when the view bound to the model is clicked.
*
* @param model The model that the view is bound to.
* @param parentView The view bound to the model which received the click.
* @param clickedView The view that received the click. This is either a child of the parentView
* or the parentView itself
* @param position The position of the model in the adapter.
*/
void onClick(T model, V parentView, View clickedView, int position);
}
以上面提到的OptionItem
為例,Epoxy會(huì)生成如下的OptionItemModel_
:
public class OptionItemModel_ extends EpoxyModel<OptionItem> {
//OnModelClickListener接口
public OptionItemModel_ onClickListener(
@Nullable final OnModelClickListener<OptionItemModel_, OptionItem> onClickListener) {
//...
}
//OnClickListener接口
public OptionItemModel_ onClickListener(@Nullable OnClickListener onClickListener) {
//...
}
}
雖然我們?cè)?code>OptionItem中定義的是OnClickListener
接口但惶,但是Epoxy會(huì)幫我們生成另外一個(gè)接口OnModelClickListener
耳鸯。通過(guò)這個(gè)接口提供的第一個(gè)參數(shù)model
,我們可以獲取當(dāng)前這個(gè)EpoxyModel的最新的屬性:
//使用 OptionItem 的代碼片段
subjects.forEach { subject ->
optionItem {
id(subject.id)
name(subject.name)
//已經(jīng)選擇的科目ID為checkedSubjectID
checked(checkedSubjectID == subject.id)
onClickListener { model, _, _, _ ->
//model指的就是當(dāng)前這個(gè)OptionItemModel_膀曾,通過(guò)其checked()方法可以獲取當(dāng)前model最新的屬性
if (!model.checked())
//...
}
}
}
通過(guò)OnModelClickListener
接口獲取的model的最新的屬性县爬,這種方式不會(huì)出現(xiàn)數(shù)據(jù)過(guò)時(shí)的問(wèn)題。
不過(guò)添谊,這種方式只適用于點(diǎn)擊事件回調(diào)财喳,對(duì)于別的回調(diào)(例如長(zhǎng)按事件回調(diào)等等),Epoxy并不會(huì)幫我們生成類似的接口。關(guān)于這個(gè)問(wèn)題更多的解決方案耳高,可以查看Epoxy的文檔扎瓶。
2. 擴(kuò)展Epoxy的使用
在Epoxy的幫助下,大部分界面的主體部分都可以使用RecyclerView來(lái)實(shí)現(xiàn)泌枪,有的界面可能看上去并不像是需要RecyclerView來(lái)實(shí)現(xiàn)的概荷,此時(shí),你可以把界面作為唯一的元素放進(jìn)RecyclerView中碌燕,這樣便于Epoxy的統(tǒng)一管理误证。但是,界面是千變?nèi)f化的修壕,有些情況下愈捅,Epoxy也顯得力不從心。
如上圖所示慈鸠,界面主體部分仍然可以使用RecyclerView蓝谨,但是,在界面的底部卻錨定著一個(gè)按鈕青团,通常情況下譬巫,我們會(huì)使用LinearLayout
裝載RecyclerView和底部按鈕,讓按鈕固定在底部就可以了督笆。這沒(méi)有太大的問(wèn)題缕题,只是在網(wǎng)絡(luò)請(qǐng)求過(guò)程中,界面顯示Loading的狀態(tài)下胖腾,底部的按鈕會(huì)顯示出來(lái)烟零。假設(shè)我們需要在網(wǎng)絡(luò)出錯(cuò)時(shí),整個(gè)界面顯示“網(wǎng)絡(luò)出錯(cuò)咸作,點(diǎn)擊重試”之類的提示锨阿,那么我們還需要控制底部按鈕是否可見(jiàn)等等。這樣的界面顯得就不那么響應(yīng)式记罚,如果能做到把底部按鈕也放進(jìn)RecyclerView進(jìn)行統(tǒng)一管理就更完美了墅诡,這樣無(wú)論網(wǎng)絡(luò)請(qǐng)求成功與否,都可以通過(guò)Epoxy管理要顯示的元素(網(wǎng)絡(luò)成功時(shí)顯示列表+按鈕桐智,失敗時(shí)顯示網(wǎng)絡(luò)錯(cuò)誤提示)末早,這樣顯然更加符合界面響應(yīng)式的思想。Epoxy其實(shí)提供了這樣的能力说庭。
如果要把底部按鈕也放進(jìn)RecyclerView中然磷,并且保持上部的仍然是個(gè)列表,需要使用Epoxy的兩個(gè)擴(kuò)展特性:
- Grouping Models
- Carousels
Grouping Models是指將多個(gè)Models結(jié)合成一組刊驴,再以組的形式交由Epoxy管理姿搜。Grouping Models的內(nèi)容較多寡润,這里就不展開(kāi)講了,具體內(nèi)容可以查看Epoxy的文檔舅柜。
Carousels本意是旋轉(zhuǎn)木馬或者跑馬燈梭纹。Epoxy幫我們大大簡(jiǎn)化了RecyclerView嵌套R(shí)ecyclerView的使用,因?yàn)槌S糜凇靶D(zhuǎn)木馬”的效果致份,所以就將這種特性稱為Carousels变抽。Carousels的內(nèi)容也很多,具體內(nèi)容可以查看Epoxy的文檔氮块。
雖然Carousels常用于“旋轉(zhuǎn)木馬”的效果,但是其本質(zhì)還是RecyclerView的嵌套雇锡,我們可以擴(kuò)展Carousels,把它用于兩個(gè)縱向的RecyclerView嵌套僚焦,并且結(jié)合Grouping Models锰提,就可以把底部按鈕也放進(jìn)RecyclerView中,并且保持上部的仍然是個(gè)RecyclerView:
/**
* RecyclerView內(nèi)部嵌套的RecyclerView(縱向)
*/
@ModelView(autoLayout = ModelView.Size.MATCH_WIDTH_MATCH_HEIGHT)
class InnerRv @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Carousel(context, attrs, defStyleAttr) {
override fun createLayoutManager(): LayoutManager {
return LinearLayoutManager(context)
}
override fun getSnapHelperFactory(): SnapHelperFactory? {
return null
}
override fun getDefaultSpacingBetweenItemsDp(): Int {
return 0
}
}
R.layout.bottom_btn_recycler_view
如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ViewStub
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:inflatedId="@+id/recyclerView">
</ViewStub>
<ViewStub
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_button_height"/>
</LinearLayout>
把底部按鈕也放入RecyclerView中:
fun bottomModelGroup(bottomModel: EpoxyModel<*>, models: List<EpoxyModel<*>>): EpoxyModelGroup {
return EpoxyModelGroup(
R.layout.bottom_btn_recycler_view,
InnerRvModel_().id(1).models(models),
bottomModel.id(2)
)
}
//真正使用
bottomModelGroup(
BottomButtonModel_(), //底部按鈕的Model
pointModels() //上部考點(diǎn)的Models
).addTo(epoxyController)
以上代碼省略了非常多的內(nèi)容芳悲,僅僅是個(gè)示例立肘。大致含義是,先通過(guò)擴(kuò)展Carousel
定義我們自己的名扛,用于縱向嵌套的RecyclerView谅年;然后通過(guò)EpoxyModelGroup
把嵌套的RecyclerView和底部的按鈕都放入外層的主RecyclerView中。這只是網(wǎng)絡(luò)請(qǐng)求成功的情況肮韧,失敗的情況下融蹂,我們可以把網(wǎng)絡(luò)錯(cuò)誤提示的Model放入主RecyclerView中,無(wú)縫切換弄企,做到真正的界面響應(yīng)式超燃。
最后一個(gè)問(wèn)題,一個(gè)縱向滑動(dòng)的RecyclerView內(nèi)部又嵌套了一個(gè)縱向滑動(dòng)的RecyclerView拘领,如果外層的RecyclerView攔截了滑動(dòng)事件意乓,那么滑動(dòng)事件將傳遞不到內(nèi)部的RecyclerView,這將導(dǎo)致內(nèi)部的RecyclerView不可滑動(dòng)约素。經(jīng)過(guò)一番嘗試后届良,發(fā)現(xiàn)可以將外層RecyclerView的LayoutManager設(shè)置為不可滑動(dòng)的,這樣外層RecyclerView就不會(huì)攔截滑動(dòng)事件了圣猎。
class NoScrollLayoutManager(context: Context) : LinearLayoutManager(context) {
override fun canScrollHorizontally() = false
override fun canScrollVertically() = false
}
以上以一個(gè)例子說(shuō)明了如果通過(guò)嵌套R(shí)ecyclerView的方式擴(kuò)展Epoxy的使用場(chǎng)景士葫,其實(shí),這不僅適用于底部有固定按鈕的情況送悔,界面頂部有什么固定元素为障,或者頂部底部都有固定元素,甚至中間有固定元素的都可以使用。
總結(jié)
本文介紹了我關(guān)于Epoxy的一些實(shí)踐經(jīng)驗(yàn)鳍怨。第一點(diǎn)是關(guān)于Epoxy點(diǎn)擊事件容易犯的錯(cuò)誤及解決方案呻右,這是很容易犯錯(cuò)的一點(diǎn),當(dāng)你的點(diǎn)擊事件跟你想要的效果不一樣的時(shí)候鞋喇,可以查看一下是不是這個(gè)地方錯(cuò)了声滥;第二點(diǎn)是如何擴(kuò)展Epoxy使用場(chǎng)景的問(wèn)題,也是我在實(shí)踐中摸索出來(lái)的方式侦香,希望對(duì)你有用落塑。Epoxy的內(nèi)容很多,其源碼也比較復(fù)雜罐韩,我知之有限憾赁,如果你有什么問(wèn)題,歡迎留言交流散吵。