前言

Android真响应式架构系列文章:

Android真响应式架构——MvRx
Epoxy——RecyclerView的绝佳助手
Android真响应式架构——Model层设计
Android真响应式架构——数据流动性
Android真响应式架构——Epoxy的使用
Android真响应式架构——MvRx和Epoxy的结合

在第一篇文章中,我就说过,MvRx界面响应式的关键在于Epoxy,并且在第二篇文章中对Epoxy的使用方式做了简单介绍。我个人认为,MvRx真正难以掌握的是Epoxy,而不是MvRx本身。你可以去查看一下MvRx的代码,真的没有几个类,也没有多少代码,还是很容易理解的。但是Epoxy就显得复杂多了,代码量及复杂程度都大大增加。虽说,MvRx将Epoxy视为可选项,但是,我觉得没有Epoxy的话,MvRx的作用将大打折扣。如果没有Epoxy,MvRx也就失去了界面响应式的能力,那么MvRx也不能称之为“真响应式架构”,虽说真不真的也没有什么意义(这个名字也是我瞎起的),至少MvRx相较于Android Architecture Component的优势就小了很多。所以,我还是推荐MvRx结合Epoxy一起使用的。
Epoxy之于MvRx的作用是毋庸置疑的,但是,Epoxy本身的复杂性也是无法回避的。关于Epoxy,我个人的理解也有限,这篇文章谈谈,我在使用Epoxy的过程中遇到的容易出错的地方,以及一种不太容易想到的使用方式。

这篇文章主要讲两点:1. Epoxy是如果设置item的点击事件的;2. 使用Epoxy对RecyclerView进行嵌套使用,以拓展Epoxy的使用范围。以上内容都是基于Epoxy的具体实践,会涉及到Epoxy的很多内容,这些内容我不可能面面俱到,希望你已经熟悉Epoxy的基本使用方式,然后再来看这篇文章。

1. 点击事件

在Epoxy——RecyclerView的绝佳助手文中,提到了如何设置点击事件,讲得很简单,实际上这是个很tricky的点。
在Epoxy中我们经常这么设置点击事件:

@CallbackPropfun onClickListener(listener: OnClickListener?) {    setOnClickListener(listener)}

这实际上等价于

@ModelProp(options = {Option.NullOnRecycle, Option.DoNotHash})fun onClickListener(listener: OnClickListener?) {    setOnClickListener(listener)}

CallbackProp注解相当于ModelProp注解设置了NullOnRecycleDoNotHash两个选项。NullOnRecycle的含义是:当View滑出屏幕,从RecyclerView解绑时,将对应的属性设为null(对于上例而言,即调用onClickListener(null));DoNotHash的含义是:该属性的hashcode发生变化时,不进行重新的绑定。这两点都非常符合类似于点击事件这样的回调。通常情况下,我们都会使用匿名内部类的方式去设置点击事件的回调,如果没有设置DoNotHash,那么每次EpoxyModels重建时(调用requestModelBuild方法),那么所有包含点击事件回调的EpoxyModel都会被认为是发生了改变的,因为点击事件的回调是以匿名内部类的方式实现的,这次匿名内部类的hashcode自然和上次的不同,这会导致几乎所有EpoxyModel都需要重新绑定到RecyclerView上。然而,一般而言,这是不必要的,因为虽然匿名内部类不相等了,但是匿名内部类表达的含义并没有改变,因此没必要重新绑定,而DoNotHash正是起到这样的作用。所以说,实际上DoNotHash或者说CallbackProp起到了一定优化的作用。
但是,这里面有个大问题,如果我们表示回调的匿名内部类捕获了外部的变量,当这个回调被调用时,这个变量可能已经过时了。来看个例子:

选择学科
@ModelViewclass 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)        //已经选择的科目ID为checkedSubjectID        checked(checkedSubjectID == subject.id)        onClickListener { view ->            //这里捕获了外层变量checkedSubjectID            if (checkedSubjectID != subject.id)            //...        }    }}

在Kotlin中我们一般使用lambda表达式来实现点击事件的回调,本质上跟匿名内部类是一样的。在该lambda表达式内部,我们捕获了外部变量checkedSubjectID,但是该变量会随着我们切换学科而改变,当点击事件发生,lambda表达式被调用时,被捕获的checkedSubjectID的值可能已经过时了。这是因为,我们使用了DoNotHash,第一次lambda表达式捕获的变量checkedSubjectID是多少,之后就总是那个值,不会改变。这显然是不行的,Epoxy的做法是使用OnModelClickListener来替代OnClickListener接口。以下是OnModelClickListener接口的定义:

/** Used to register a click listener on a generated model. */public interface OnModelClickListener, 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会生成如下的OptionItemModel_

public class OptionItemModel_ extends EpoxyModel {  //OnModelClickListener接口  public OptionItemModel_ onClickListener(      @Nullable final OnModelClickListener onClickListener) {    //...  }  //OnClickListener接口  public OptionItemModel_ onClickListener(@Nullable OnClickListener onClickListener) {    //...  }}

虽然我们在OptionItem中定义的是OnClickListener接口,但是Epoxy会帮我们生成另外一个接口OnModelClickListener。通过这个接口提供的第一个参数model,我们可以获取当前这个EpoxyModel的最新的属性:

//使用 OptionItem 的代码片段subjects.forEach { subject ->    optionItem {        id(subject.id)        name(subject.name)        //已经选择的科目ID为checkedSubjectID        checked(checkedSubjectID == subject.id)        onClickListener { model, _, _, _ ->            //model指的就是当前这个OptionItemModel_,通过其checked()方法可以获取当前model最新的属性            if (!model.checked())            //...        }    }}

通过OnModelClickListener接口获取的model的最新的属性,这种方式不会出现数据过时的问题。
不过,这种方式只适用于点击事件回调,对于别的回调(例如长按事件回调等等),Epoxy并不会帮我们生成类似的接口。关于这个问题更多的解决方案,可以查看Epoxy的文档。

2. 扩展Epoxy的使用

在Epoxy的帮助下,大部分界面的主体部分都可以使用RecyclerView来实现,有的界面可能看上去并不像是需要RecyclerView来实现的,此时,你可以把界面作为唯一的元素放进RecyclerView中,这样便于Epoxy的统一管理。但是,界面是千变万化的,有些情况下,Epoxy也显得力不从心。

底部有按钮

如上图所示,界面主体部分仍然可以使用RecyclerView,但是,在界面的底部却锚定着一个按钮,通常情况下,我们会使用LinearLayout装载RecyclerView和底部按钮,让按钮固定在底部就可以了。这没有太大的问题,只是在网络请求过程中,界面显示Loading的状态下,底部的按钮会显示出来。假设我们需要在网络出错时,整个界面显示“网络出错,点击重试”之类的提示,那么我们还需要控制底部按钮是否可见等等。这样的界面显得就不那么响应式,如果能做到把底部按钮也放进RecyclerView进行统一管理就更完美了,这样无论网络请求成功与否,都可以通过Epoxy管理要显示的元素(网络成功时显示列表+按钮,失败时显示网络错误提示),这样显然更加符合界面响应式的思想。Epoxy其实提供了这样的能力。
如果要把底部按钮也放进RecyclerView中,并且保持上部的仍然是个列表,需要使用Epoxy的两个扩展特性:

  1. Grouping Models
  2. Carousels

Grouping Models是指将多个Models结合成一组,再以组的形式交由Epoxy管理。Grouping Models的内容较多,这里就不展开讲了,具体内容可以查看Epoxy的文档。

Carousels本意是旋转木马或者跑马灯。Epoxy帮我们大大简化了RecyclerView嵌套RecyclerView的使用,因为常用于“旋转木马”的效果,所以就将这种特性称为Carousels。Carousels的内容也很多,具体内容可以查看Epoxy的文档。

旋转木马效果,纵向RecyclerView嵌套横向RecyclerView

虽然Carousels常用于“旋转木马”的效果,但是其本质还是RecyclerView的嵌套,我们可以扩展Carousels,把它用于两个纵向的RecyclerView嵌套,并且结合Grouping Models,就可以把底部按钮也放进RecyclerView中,并且保持上部的仍然是个RecyclerView:

/** * RecyclerView内部嵌套的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"?>            

把底部按钮也放入RecyclerView中:

fun bottomModelGroup(bottomModel: EpoxyModel<*>, models: List>): EpoxyModelGroup {    return EpoxyModelGroup(        R.layout.bottom_btn_recycler_view,        InnerRvModel_().id(1).models(models),        bottomModel.id(2)    )}//真正使用bottomModelGroup(    BottomButtonModel_(), //底部按钮的Model    pointModels() //上部考点的Models).addTo(epoxyController)

以上代码省略了非常多的内容,仅仅是个示例。大致含义是,先通过扩展Carousel定义我们自己的,用于纵向嵌套的RecyclerView;然后通过EpoxyModelGroup把嵌套的RecyclerView和底部的按钮都放入外层的主RecyclerView中。这只是网络请求成功的情况,失败的情况下,我们可以把网络错误提示的Model放入主RecyclerView中,无缝切换,做到真正的界面响应式。

嵌套的RecyclerView

最后一个问题,一个纵向滑动的RecyclerView内部又嵌套了一个纵向滑动的RecyclerView,如果外层的RecyclerView拦截了滑动事件,那么滑动事件将传递不到内部的RecyclerView,这将导致内部的RecyclerView不可滑动。经过一番尝试后,发现可以将外层RecyclerView的LayoutManager设置为不可滑动的,这样外层RecyclerView就不会拦截滑动事件了。

class NoScrollLayoutManager(context: Context) : LinearLayoutManager(context) {    override fun canScrollHorizontally() = false    override fun canScrollVertically() = false}

以上以一个例子说明了如果通过嵌套RecyclerView的方式扩展Epoxy的使用场景,其实,这不仅适用于底部有固定按钮的情况,界面顶部有什么固定元素,或者顶部底部都有固定元素,甚至中间有固定元素的都可以使用。

顶部固定 顶部底部均固定

总结

本文介绍了我关于Epoxy的一些实践经验。第一点是关于Epoxy点击事件容易犯的错误及解决方案,这是很容易犯错的一点,当你的点击事件跟你想要的效果不一样的时候,可以查看一下是不是这个地方错了;第二点是如何扩展Epoxy使用场景的问题,也是我在实践中摸索出来的方式,希望对你有用。Epoxy的内容很多,其源码也比较复杂,我知之有限,如果你有什么问题,欢迎留言交流。

更多相关文章

  1. 党系列之五:Android(安卓)UI相关知识总结 Dear_HS Android伸手党
  2. 自定义背景文件,android:shape的使用!
  3. Android(安卓)使用shape来优化界面效果
  4. Android使用AudioRecord遇到的问题与解决方法
  5. android应用程序键盘事件机制
  6. Android(安卓)4编程入门经典—开发智能手机与平板电脑应用
  7. Android(安卓)GridView 使用示例
  8. android之使用mvn构建创造项目步骤
  9. Java事件模型与Android事件模型的比较

随机推荐

  1. 【Android Linux内存及性能优化】(八) 系
  2. IOS之UITabBarViewController用法
  3. Android下实现一个Activity的全屏显示
  4. 关于build.gradle配置文件详细参数讲解
  5. Android知识体系总结(全方面覆盖Android
  6. Android(安卓)EditText OnTouchListener
  7. android notification,notificationmanage
  8. Android wifi信号强度与图标对应关系
  9. 【阿里聚安全·安全周刊】一种秘密窃取数
  10. Android IPC(一)