Android真響應(yīng)式架構(gòu)——Epoxy的使用

前言

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è)置了NullOnRecycleDoNotHash兩個(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è)例子:

選擇學(xué)科
@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ò)展特性:

  1. Grouping Models
  2. 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的文檔氮块。

旋轉(zhuǎn)木馬效果瞬沦,縱向RecyclerView嵌套橫向RecyclerView

雖然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)式超燃。

嵌套的RecyclerView

最后一個(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)題,歡迎留言交流散吵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末龙考,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子矾睦,更是在濱河造成了極大的恐慌晦款,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枚冗,死亡現(xiàn)場(chǎng)離奇詭異缓溅,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)赁温,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門坛怪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人股囊,你說(shuō)我怎么就攤上這事酝陈。” “怎么了毁涉?”我有些...
    開(kāi)封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵沉帮,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我贫堰,道長(zhǎng)穆壕,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任其屏,我火速辦了婚禮喇勋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘偎行。我一直安慰自己川背,他們只是感情好贰拿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著熄云,像睡著了一般膨更。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缴允,一...
    開(kāi)封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天荚守,我揣著相機(jī)與錄音,去河邊找鬼练般。 笑死矗漾,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的薄料。 我是一名探鬼主播敞贡,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼摄职!你這毒婦竟也來(lái)了誊役?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤琳钉,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蛛倦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體歌懒,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年溯壶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了及皂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡且改,死狀恐怖验烧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情又跛,我是刑警寧澤碍拆,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站慨蓝,受9級(jí)特大地震影響感混,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜礼烈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一弧满、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧此熬,春花似錦庭呜、人聲如沸滑进。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扶关。三九已至,卻和暖如春近哟,著一層夾襖步出監(jiān)牢的瞬間驮审,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工吉执, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留疯淫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓戳玫,卻偏偏與公主長(zhǎng)得像熙掺,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咕宿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容