Writing Better Adapters

作者: 唐先僧 | 来源:发表于2017-02-12 16:09 被阅读122次

    实现Adapter是Android开发者最频繁的任务之一。它是所有列表的基础。纵观所有的应用,列表是大多数应用的基础。

    我们在实现列表视图时所遵循的架构通常都是一样的:一个View和一个持有数据的adapter。一直这么做会导致我们无视自己所写的东西,甚至是一些丑陋的代码。甚至更糟,我们一再重复写这些糟糕的代码。

    现在该仔细的看一看adapter了。

    RecyclerView基础

    RecycleViewListView同样适用)的基本操作有:

    • 创建 view 和 持有view信息的 * ViewHolder*。
    • ViewHolder 绑定到adapter持有的数据,通常是一个model类的列表。

    实现这些功能非常直观,基本上不会出错。

    拥有各种类型的RecyclerView

    当你需要在视图里加入各种部件时,情况就变得棘手了。当你使用CardViews或者在列表元素之间缀入广告时,卡片的类型可能不同。你也许会使用一个类型完全不同的对象列表(本文使用Kotlin,但是它可以很简单的转换成Java,因为没有使用独有语言特性)。

    interface Animal
    class Mouse: Animal
    class Duck: Animal
    class Dog: Animal
    class Car
    

    你在清点各种动物时突然发现了毫不相关的东西,比如车。

    在这些情况下你可能有不同的view类型需要展示。也就意味着你需要创建不同的ViewHolders并且可能要分别关联不同的布局。API将类型标识符定义为整型,这是一切丑陋开始的地方。

    我们来看一看代码,当你有多种item类型的时候,你通过重载声明函数:

    override fun getItemViewType(position: Int) : Int
    

    该函数的默认实现总是返回0。实现者需要将指定位置的view类型转换为整型数据。

    下一步:创建ViewHolders。所以你必须实现:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
    

    在这个方法中API传入一个整型数据作为参数,这个整型数据来自前面的getItemViewType。这个方法的实现非常琐碎:用一个switch语句,或者类似的语句来为每一个给定的类型创建一个ViewHolder。

    差别来自当绑定新创建的ViewHolder时。

    override fun onBindViewHolder(holder: ViewHolder, position: Int): Any
    

    注意这里没有类型参数。如果需要你可以使用getItemViewType,不过通常情况下不需要。你可以在所有不同类型的ViewHolders的基类中定义一些bind()方法供你调用。

    丑陋

    所以现在的问题是什么?实现看起来很简单,不是吗?

    我们一起再来看一看getItemViewType().

    系统需要知道每一个位置的类型。所以你需要将model列表中的每一项转换成一个view类型。

    你也许会写下类似这样的代码:

    if (things.get(position) is Duck) {
        return TYPE_DUCK
    } else if (things.get(position) is Mouse) {
        return TYPE_MOUSE
    }
    

    我们能说这很丑陋吗?

    如果你所有的ViewHolder没有一个公共的基类也许会变得更糟糕。如果列表中的数据是全不同的类型,当你把它们绑定到ViewHolder时也会有同样丑陋的代码:

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val thing = things.get(position)
        if (thing is Animal) {
            (holder as AnimalViewHolder).bind(thing as Animal)
        } else if (thing is Car) {
            (holder as CarViewHolder).bind(thing as Car)
        }
    ...
    }
    

    这是一团乱麻。instance-of检查和一堆强制类型转换。两个都是代码异味并且都被认为是反模式

    多年前我在我的显示器上贴了很多引言。其中一条来自Effective C++ by Scott Meyers,大概意思就是:

    每当你发现自己写出这种形式的代码时“如果对象是T1类型,然后做什么事情,但是如果对象是T2类型,则做其他的事情”,抽自己一巴掌吧。

    如果你看一看前面adapter的实现,需要抽自己很多下。

    • 我们使用了类型检查并使用了很多丑陋的类型转换
    • 这不是面向对象的代码!面向对象刚刚庆祝了其50岁生日,所以我们应该尽可能多的使用其效力
    • 除此之外,我们实现adapter的方式违反了SOLID原则中的开闭原则。该原则要求:“对扩展开放但是对修改关闭”

    但是我们需要添加新的类型,添加另外一个Model时,比如说RabbitRabbitViewHolder,我们必须修改adapter中的很多方法。在很显然违反了开闭原则。新类型对象的添加不应该导致修改已有代码中的方法。

    所以让我们尝试解决这个问题。

    一起来解决问题

    一个替代方案就是在中间层放入一些东西来帮我们做转换。最简单的方式就是将你所有的类型放到一个Map中,然后通过一次调用获取其类型。应该就像这样:

    override fun getItemViewType(position: Int) : Int 
       = types.get(things.javaClass)
    

    现在好多了,不是吗?
    坏消息是:不完全是!最后也仅仅是隐藏了类型检查。

    你会怎么实现我们前面提到的onBindViewholder()?应该就像这样:if object is of type T1 then do.. else…这里还是要抽自己巴掌。

    我们的目标是要能够做到添加新的类型而不用修改adapter

    所以:不要在model和view之间的adapter中创建你自己的类型映射。Google建议使用layout id。使用这个技巧你需要简单的使用创建view的layout id就可以,而不用手动创建类型映射。当然从性能考虑你也可以将其保存到enum中。

    但是你仍然需要做类型映射?怎么实现?

    最后的最后你还是需要将model映射到view。能够将这个映射移动到model中吗?

    将类型放置到model中好像很诱人,就像这样:

    fun getType() : Int = R.layout.item_duck
    

    这种方式实现的adapter完全是通用的了:

    override fun getItemViewType(pos: Int) = things[pos].getType()
    

    应用了开闭原则,当添加新的类型时不需要做任何修改。

    但是现在个层已经完全糅合到一起了,实际上破坏了整体架构。实体类知道了展现层的信息,这将我们引向了错误的方向。这对我们来说是不可接受的
    再次说明:通过向一个对象添加方法来获取其类型不是面向对象的编程。你再一次简单的隐藏了类型检查而已。

    ViewModel

    处理这样另一个方法就是:使用独立的ViewModel而不是直接使用我们的Model。最终问题变成了我们的model是不相交的,他们没有共同的基类:汽车不是动物。对于数据层是对的。你仅仅在表示层列表展示中使用。所以当你向这一层添加新的类型时就不存在这个问题,他们有共同的基类。

    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中。

    这种方式你可以在ViewModel中添加所有的格式化逻辑还可以使用Android的Data Binding 库

    在adapter中使用ViewModel而不是Modle的思路在我们需要一些伪造项目比如分割线,小节header或者简单的广告项目时非常有用。

    这是解决问题的一种方法。但是这不是唯一的方法。

    访问者

    让我们回到最初的仅仅使用Model的思路上。如果你有许多model类,也许你不想创建很多ViewModel。
    考虑你在model中首先添加的type()方法,这个方法耦合性太强了。你应该避免直接在type()方法中使用展现层的代码。你需要间接的使用,将实际的类型获取移到别的地方。在tpe()方法中添加一个接口怎么样:

    interface Visitable {
        fun type(typeFactory: TypeFactory) : Int
    }
    

    现在也许你会说在这里引入的工厂也许还是会像最初的版本一样使用switch语句,对吗?

    不会的!这种方法是基于访问者模式,经典的Gang-of-Four pattern之一。所有model所要做的就是传递这个type调用:

    interface Animal : Visitable
    interface Car : Visitable
    
    class Mouse: Animal {
        override fun type(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
    }
    

    这种方法是完全类型安全,完全没有类型判断也没有类型转换。

    并且工厂的职责非常清晰:它知道所有的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实现,基本上不需要再做修改。

    结论

    • 尽量保持展现层的代码整洁。
    • Instance-of检查应该列入红色警告标志!
    • 小心向下的类型转换,因为这是不好的代码味道。
    • 尝试用正确的OO方法替换前面两条。考虑使用接口和继承
    • 尝试使用通用方法避免类型转换。
    • 使用ViewModel。
    • 检查访问者模式的用法。

    我非常愿意学习其他可以是Adapter更简洁的思路。

    最后非常感谢Jan MDmitri Kudrenko,他们在Github上使用Java和Kotlin创建了示例:

    https://github.com/dmitrikudrenko/BetterAdapters
    https://github.com/meierjan/BetterAdapters

    本文译自Writing Better Adapters

    相关文章

      网友评论

        本文标题:Writing Better Adapters

        本文链接:https://www.haomeiwen.com/subject/xeuaittx.html