- 原文地址:Writing Better Adapters
- 原文作者:Danny Preussler
- 译文出自:掘金翻译计划
- 译者:Siegen
- 校对者:Liz,张拭心
对Android 开发者来说实现 adapter 是最常见的任务之一。它是每一个列表的基础。看看市面上的应用,列表是大部分应用的基础。
我们实现列表 view 的方式通常是一样的:一个 view 搭配一个装载着数据的 adapter。一直这样做可能会让我们忽视了我们正在写的东西,甚至是糟糕的代码。更糟的是,我们通常会一直重复那些糟糕的代码。
是时候仔细看看这些 adapter 。
RecyclerView ( ListView 也适用)基本使用方式如下:
实现这些操作一气呵成并且也不会出现太多错误。
当你在你的 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 的实现,应该有许多的耳光需要你去扇了。
一个替代方案是在中间添加一个东西为我们做转换。这跟把你的 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 而不是直接使用我们的 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 拥有一个非常通用的实现,并且几乎不需要变化。
我很乐意了解到更多其他的想法来使我们的 adapter 保持整洁。
作者:siegen
链接:http://www.jianshu.com/p/c6a44e18badb
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。