- 原文作者:Danny Preussler
- 译文出自:掘金翻译计划
- 译者:Siegen
- 校对者:Liz,张拭心
对Android 开发者来说实现 adapter 是最常见的任务之一。它是每一个列表的基础。看看市面上的应用,列表是大部分应用的基础。
我们实现列表 view 的方式通常是一样的:一个 view 搭配一个装载着数据的 adapter。一直这样做可能会让我们忽视了我们正在写的东西,甚至是糟糕的代码。更糟的是,我们通常会一直重复那些糟糕的代码。
是时候仔细看看这些 adapter 。
RecyclerView 的基本操作
RecyclerView ( ListView 也适用)基本使用方式如下:
- 创建 view 以及容纳 view 信息的 ViewHolder 。
- 把 ViewHolder 与 adapter 装载的数据相绑定,这些数据可能是一系列的 model 类。
实现这些操作一气呵成并且也不会出现太多错误。
有着不同类型的 RecyclerView
当你在你的 view 里需要有不同类型的 item(条目)时,实现 adapter 会变得更加困难。也许是因为你使用 CardView 或者你需要在你的控件里插入广告,使得基础的 item 有了不同类型的卡片样式。甚至你可能有一系列完全不同类型的对象(本文使用 Kotlin 来举例,但是它可以被轻松的应用到 Java 中,因为在这里没有使用 kotlin 特有的语法。)
interface Animal
class Mouse: Animal
class Duck: Animal
class Dog: Animal
class Car
在这里,你有好几种动物,然后突然出现了一个完全不相干的汽车。
在这个使用情况里,你可能用不同的 view 类型用来展示。 这意味着你可能还需要在每个 ViewHolder 中解析不同的布局。API 把类型的标识码定义为 integers(整型数),这就是糟糕代码开始的地方!
让我们来看一些代码。当你的 item 有两个以上的类型时,,由于它们的默认实现总是返回零,你通常需要通过覆写这个方法来声明它们:
override fun getItemViewType(position: Int) : Int
这个实现把类型转换成 Integer 值。
下一步:创建 ViewHolder。你不得不实现下面这个方法:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
在这个方法里,API 把你之前传递的 Integer 类型作为参数。接下来的实现非常常见:用一个 switch 语句,或者类似的东西(if-else),为每个给定类型创建对应的 ViewHolder 。
不同的地方在于当绑定新创建的(或者复用的)ViewHolder 的时候:
override fun onBindViewHolder(holder: ViewHolder, position: Int): Any
注意这里没有类型参数。如果有必要的话你可以使用 getItemViewType 方法,但通常这是没必要的。在所有 ViewHolder 的基类里,你可以做绑定 bind () 操作。
槽糕之处
所以现在的问题是什么?这样做看起来很容易实现,不是么?
让我们再看一次 getItemViewType()。
这个系统需要每个位置的类型。所以你不得不在你背后的 model 列表中,把一个 item 转成一个 view 类型。
你可能想要这样写:
if (things.get(position) is Duck) {
return TYPE_DUCK
} else if (things.get(position) is Mouse) {
return TYPE_MOUSE
}
这样写代码真的很糟糕。如果你的 ViewHolder 没有继承自一个共同基础类,这会变得更糟。当你绑定 ViewHolder 的时候,如果它们是完全不同的类型,在你的列表中你会有同样糟糕的代码。
许多的 instance-of 检查和转型,这真是一团糟。这两个都是坏代码的味道,这种写法,通常被认为是反面模式的例子。
许多年前,我在我的显示器上贴了许多的名言。其中的一个来自 Scott Meyers 写的《Effective C++》 这本书(最好的IT书籍之一),它是这么说的:
不管什么时候,只要你发现自己写的代码类似于 “ if the object is of type T1, then do something, but if it’s of type T2, then do something else ”,就给自己一耳光。
如果你看到那些 adapter 的实现,应该有许多的耳光需要你去扇了。
- 我们有类型检查并且我们有许多糟糕的转型。
- 这完全不是面向对象的代码。面向对象编程刚刚庆祝了它的 50 岁生日,我们应该尽力去发挥它的长处。
- 另外,我们实行那些 adapter 的方法违背了 SOLID 原则中的“开闭准则” 。它是这样说的:“对扩展开放,对修改封闭。” 当我们添加另一个类型或者 model 到我们的类中时,比如叫 Rabbit 和 RabbitViewHolder,我们不得不在 adapter 里改变许多的方法。 这是对开闭原则明显的违背。添加新对象不应该修改已存在的方法。
让我们解决这个问题
一个替代方案是在中间添加一个东西为我们做转换。这跟把你的 Class 类型放入到 Map 中一样简单并且可以通过函数调用来获取相应的类型。这个方案基本是这样的:
override fun getItemViewType(position: Int) : Int
= types.get(things.javaClass)
现在它已经好多了,不是么?答案令人难过:这并不够好!这个方案只是把 instance-of 检查隐藏了起来而已。
你会如何实现上文提到的 onBindViewholder() 方法?可能会是这样:if object is of type T1 then do.. else… ,这样你仍然需要给自己一耳光。
我们的目标应该是在不修改 adapter 的情况下能够添加新的类型。
所以:不要一开始就在 view 和 model 之间的 adapter 里创建你自己的类型映射。Google 建议使用布局 id。利用这个技巧,你可以简单的使用你正在填充的布局 id 而不需要人为制作类型映射。当然你可能会把另一个枚举类型保存成 perfmatters。
但是你仍然需要把它们互相关联到一起么?要怎么做呢?
在最后你需要把 model 与 view 关联在一起。这里面的关联信息能够迁移到 model 里面吗?
把 item 类型放进你的 model 里是很诱人的,就像这样。
fun getType() : Int = R.layout.item_duck
这种 adapter 类型的实现方式是完全通用的:
override fun getItemViewType(pos: Int) = things[pos].getType()
开闭原则被应用了,当添加新的 model 时无需做多余的改变。
但是这样做,布局层完全混合在一起不说,还破坏了整体结构。实体直接对外展示,这样的展示方向是错误的。这对我们来说是完全不能接受的。并且:在一个对象里面添加方法来询问它的类型,这不是面向对象。你只是再一次的隐藏了 instance-of 检查而已。
ViewModel
解决这个问题的一个方法是:拥有独立的 ViewModel 而不是直接使用我们的 Model。我们的问题是我们的 model 是互不关联的,他们没有一个共同的基类:一辆车不是一个动物。这是对的。只有 presenter 层你需要在列表里展示它们。所以当你为 presenter 层展示这些 model 时没有这个问题,他们可以拥有一个共同的基类也就是 ViewModel。
abstract class ViewModel {
abstract fun type(): Int
}
class DuckViewModel(val duck: Duck): ViewModel() {
override fun type() = R.layout.duck
}
class CarViewModel(val car: Car): ViewModel() {
override fun type() = R.layout.car
}
所以你可以简单包装下 model ,完全不需要修改它们,然后在新的 ViewModel 中保留它对应的 model ,这样你还可以添加所有的逻辑代码并且还能使用 Android 最新的 Data Binding Library。
在 adapter 里使用 ViewModel list 而不是 Model 的这个点子很有用,尤其是当你需要额外添加的 item 的时候,类似 divider ,header或者只是广告 item。
这是解决这个问题的一个方法,但不是唯一的一个。
访问者模式
让我们回归原点,只使用 Model。假如你有许多的 model 类,不想为每一个 model 创建对应的 ViewModel。想想最开始 model 里的 type() 方法,这个过程缺失了必要的解耦。要避免在 model 里直接写入 presenter 层的代码,间接的使用它,把实际的类型信息迁移到其他地方。那么不如在 type() 方法里添加一个接口:
interface Visitable {
fun type(typeFactory: TypeFactory) : Int
}
现在你可能会问你在这里这样做有什么好处,因为工厂方法仍然需要给不同的 item 类型分流,就像在最开始的时候 adapter 做的一样,是这样么?
不,这完全不一样!这个方法是建立在访问者模式之上的,一个典型的四人帮设计模式。所有的 model 都会调用如下方法::
interface Animal : Visitable
interface Car : Visitable
class Mouse: Animal {
override funtype(typeFactory: TypeFactory)
= typeFactory.type(this)
}
这个工厂方法拥有你需要的变化:
interface TypeFactory {
fun type(duck: Duck): Int
fun type(mouse: Mouse): Int
fun type(dog: Dog): Int
fun type(car: Car): Int
}
这种方式是完全的类型安全,没有 instance-of 检查,也根本不需要转型。
这个工厂方法的责任是明确的:它知道所有的 view 类型:
class TypeFactoryForList : TypeFactory {
override fun type(duck: Duck) = R.layout.duck
override fun type(mouse: Mouse) = R.layout.mouse
override fun type(dog: Dog) = R.layout.dog
override fun type(car: Car) = R.layout.car
我也可以创建 ViewHolder 在某个地方持有关于布局 id 的信息。所以当添加一个新 view 的时候,这个地方也跟着添加。这是相当符合 SOLID 原则的。你可能需要为新的类型创建另一个方法,但是不修改任何存在的方法:对扩展开放,对修改封闭。
现在你可能会问:为什么不直接在 adapter 里使用工厂方法而是间接的使用 model 呢?通过这个方式你可以不需要转型和类型检查就可以确保类型安全。花点时间在这里实现它,这不是一个需要的转型!间接引用正是访问者模式背后的魔法。
通过这个方法使得 adapter 拥有一个非常通用的实现,并且几乎不需要变化。
结论
- 尽力保持你的 presenter 层代码干净。
- Instance-of 检查应该是一个警告标志,尽量不要使用!
- 注意向下转型,因为这是坏代码的味道.
- 尽量把上面两个替换成正确的面向对象用法。考虑下接口和继承。
- 尽量使用通用的方式来避免转型。
- 使用 ViewModel。
- 检查访问者模式的使用方式。
我很乐意了解到更多其他的想法来使我们的 adapter 保持整洁。