- 原文作者:Danny Preussler
- 譯文出自:掘金翻譯計(jì)劃
- 譯者:Siegen
- 校對者:Liz,張拭心
對Android 開發(fā)者來說實(shí)現(xiàn) adapter 是最常見的任務(wù)之一。它是每一個(gè)列表的基礎(chǔ)??纯词忻嫔系膽?yīng)用,列表是大部分應(yīng)用的基礎(chǔ)。
我們實(shí)現(xiàn)列表 view 的方式通常是一樣的:一個(gè) view 搭配一個(gè)裝載著數(shù)據(jù)的 adapter。一直這樣做可能會讓我們忽視了我們正在寫的東西,甚至是糟糕的代碼。更糟的是,我們通常會一直重復(fù)那些糟糕的代碼。
是時(shí)候仔細(xì)看看這些 adapter 。
RecyclerView 的基本操作
RecyclerView ( ListView 也適用)基本使用方式如下:
- 創(chuàng)建 view 以及容納 view 信息的 ViewHolder 。
- 把 ViewHolder 與 adapter 裝載的數(shù)據(jù)相綁定,這些數(shù)據(jù)可能是一系列的 model 類。
實(shí)現(xiàn)這些操作一氣呵成并且也不會出現(xiàn)太多錯(cuò)誤。
有著不同類型的 RecyclerView
當(dāng)你在你的 view 里需要有不同類型的 item(條目)時(shí),實(shí)現(xiàn) adapter 會變得更加困難。也許是因?yàn)槟闶褂?CardView 或者你需要在你的控件里插入廣告,使得基礎(chǔ)的 item 有了不同類型的卡片樣式。甚至你可能有一系列完全不同類型的對象(本文使用 Kotlin 來舉例,但是它可以被輕松的應(yīng)用到 Java 中,因?yàn)樵谶@里沒有使用 kotlin 特有的語法。)
interface Animal
class Mouse: Animal
class Duck: Animal
class Dog: Animal
class Car
在這里,你有好幾種動物,然后突然出現(xiàn)了一個(gè)完全不相干的汽車。
在這個(gè)使用情況里,你可能用不同的 view 類型用來展示。 這意味著你可能還需要在每個(gè) ViewHolder 中解析不同的布局。API 把類型的標(biāo)識碼定義為 integers(整型數(shù)),這就是糟糕代碼開始的地方!
讓我們來看一些代碼。當(dāng)你的 item 有兩個(gè)以上的類型時(shí),,由于它們的默認(rèn)實(shí)現(xiàn)總是返回零,你通常需要通過覆寫這個(gè)方法來聲明它們:
override fun getItemViewType(position: Int) : Int
這個(gè)實(shí)現(xiàn)把類型轉(zhuǎn)換成 Integer 值。
下一步:創(chuàng)建 ViewHolder。你不得不實(shí)現(xiàn)下面這個(gè)方法:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
在這個(gè)方法里,API 把你之前傳遞的 Integer 類型作為參數(shù)。接下來的實(shí)現(xiàn)非常常見:用一個(gè) switch 語句,或者類似的東西(if-else),為每個(gè)給定類型創(chuàng)建對應(yīng)的 ViewHolder 。
不同的地方在于當(dāng)綁定新創(chuàng)建的(或者復(fù)用的)ViewHolder 的時(shí)候:
override fun onBindViewHolder(holder: ViewHolder, position: Int): Any
注意這里沒有類型參數(shù)。如果有必要的話你可以使用 getItemViewType 方法,但通常這是沒必要的。在所有 ViewHolder 的基類里,你可以做綁定 bind () 操作。
槽糕之處
所以現(xiàn)在的問題是什么?這樣做看起來很容易實(shí)現(xiàn),不是么?
讓我們再看一次 getItemViewType()。
這個(gè)系統(tǒng)需要每個(gè)位置的類型。所以你不得不在你背后的 model 列表中,把一個(gè) item 轉(zhuǎn)成一個(gè) view 類型。
你可能想要這樣寫:
if (things.get(position) is Duck) {
return TYPE_DUCK
} else if (things.get(position) is Mouse) {
return TYPE_MOUSE
}
這樣寫代碼真的很糟糕。如果你的 ViewHolder 沒有繼承自一個(gè)共同基礎(chǔ)類,這會變得更糟。當(dāng)你綁定 ViewHolder 的時(shí)候,如果它們是完全不同的類型,在你的列表中你會有同樣糟糕的代碼。
許多的 instance-of 檢查和轉(zhuǎn)型,這真是一團(tuán)糟。這兩個(gè)都是壞代碼的味道,這種寫法,通常被認(rèn)為是反面模式的例子。
許多年前,我在我的顯示器上貼了許多的名言。其中的一個(gè)來自 Scott Meyers 寫的《Effective C++》 這本書(最好的IT書籍之一),它是這么說的:
不管什么時(shí)候,只要你發(fā)現(xiàn)自己寫的代碼類似于 “ if the object is of type T1, then do something, but if it’s of type T2, then do something else ”,就給自己一耳光。
如果你看到那些 adapter 的實(shí)現(xiàn),應(yīng)該有許多的耳光需要你去扇了。
- 我們有類型檢查并且我們有許多糟糕的轉(zhuǎn)型。
- 這完全不是面向?qū)ο蟮拇a。面向?qū)ο缶幊虅倓倯c祝了它的 50 歲生日,我們應(yīng)該盡力去發(fā)揮它的長處。
- 另外,我們實(shí)行那些 adapter 的方法違背了 SOLID 原則中的“開閉準(zhǔn)則” 。它是這樣說的:“對擴(kuò)展開放,對修改封閉?!?當(dāng)我們添加另一個(gè)類型或者 model 到我們的類中時(shí),比如叫 Rabbit 和 RabbitViewHolder,我們不得不在 adapter 里改變許多的方法。 這是對開閉原則明顯的違背。添加新對象不應(yīng)該修改已存在的方法。
讓我們解決這個(gè)問題
一個(gè)替代方案是在中間添加一個(gè)東西為我們做轉(zhuǎn)換。這跟把你的 Class 類型放入到 Map 中一樣簡單并且可以通過函數(shù)調(diào)用來獲取相應(yīng)的類型。這個(gè)方案基本是這樣的:
override fun getItemViewType(position: Int) : Int
= types.get(things.javaClass)
現(xiàn)在它已經(jīng)好多了,不是么?答案令人難過:這并不夠好!這個(gè)方案只是把 instance-of 檢查隱藏了起來而已。
你會如何實(shí)現(xiàn)上文提到的 onBindViewholder() 方法?可能會是這樣:if object is of type T1 then do.. else… ,這樣你仍然需要給自己一耳光。
我們的目標(biāo)應(yīng)該是在不修改 adapter 的情況下能夠添加新的類型。
所以:不要一開始就在 view 和 model 之間的 adapter 里創(chuàng)建你自己的類型映射。Google 建議使用布局 id。利用這個(gè)技巧,你可以簡單的使用你正在填充的布局 id 而不需要人為制作類型映射。當(dāng)然你可能會把另一個(gè)枚舉類型保存成 perfmatters。
但是你仍然需要把它們互相關(guān)聯(lián)到一起么?要怎么做呢?
在最后你需要把 model 與 view 關(guān)聯(lián)在一起。這里面的關(guān)聯(lián)信息能夠遷移到 model 里面嗎?
把 item 類型放進(jìn)你的 model 里是很誘人的,就像這樣。
fun getType() : Int = R.layout.item_duck
這種 adapter 類型的實(shí)現(xiàn)方式是完全通用的:
override fun getItemViewType(pos: Int) = things[pos].getType()
開閉原則被應(yīng)用了,當(dāng)添加新的 model 時(shí)無需做多余的改變。
但是這樣做,布局層完全混合在一起不說,還破壞了整體結(jié)構(gòu)。實(shí)體直接對外展示,這樣的展示方向是錯(cuò)誤的。這對我們來說是完全不能接受的。并且:在一個(gè)對象里面添加方法來詢問它的類型,這不是面向?qū)ο?。你只是再一次的隱藏了 instance-of 檢查而已。
ViewModel
解決這個(gè)問題的一個(gè)方法是:擁有獨(dú)立的 ViewModel 而不是直接使用我們的 Model。我們的問題是我們的 model 是互不關(guān)聯(lián)的,他們沒有一個(gè)共同的基類:一輛車不是一個(gè)動物。這是對的。只有 presenter 層你需要在列表里展示它們。所以當(dāng)你為 presenter 層展示這些 model 時(shí)沒有這個(gè)問題,他們可以擁有一個(gè)共同的基類也就是 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 中保留它對應(yīng)的 model ,這樣你還可以添加所有的邏輯代碼并且還能使用 Android 最新的 Data Binding Library。
在 adapter 里使用 ViewModel list 而不是 Model 的這個(gè)點(diǎn)子很有用,尤其是當(dāng)你需要額外添加的 item 的時(shí)候,類似 divider ,header或者只是廣告 item。
這是解決這個(gè)問題的一個(gè)方法,但不是唯一的一個(gè)。
訪問者模式
讓我們回歸原點(diǎn),只使用 Model。假如你有許多的 model 類,不想為每一個(gè) model 創(chuàng)建對應(yīng)的 ViewModel。想想最開始 model 里的 type() 方法,這個(gè)過程缺失了必要的解耦。要避免在 model 里直接寫入 presenter 層的代碼,間接的使用它,把實(shí)際的類型信息遷移到其他地方。那么不如在 type() 方法里添加一個(gè)接口:
interface Visitable {
fun type(typeFactory: TypeFactory) : Int
}
現(xiàn)在你可能會問你在這里這樣做有什么好處,因?yàn)楣S方法仍然需要給不同的 item 類型分流,就像在最開始的時(shí)候 adapter 做的一樣,是這樣么?
不,這完全不一樣!這個(gè)方法是建立在訪問者模式之上的,一個(gè)典型的四人幫設(shè)計(jì)模式。所有的 model 都會調(diào)用如下方法::
interface Animal : Visitable
interface Car : Visitable
class Mouse: Animal {
override funtype(typeFactory: TypeFactory)
= typeFactory.type(this)
}
這個(gè)工廠方法擁有你需要的變化:
interface TypeFactory {
fun type(duck: Duck): Int
fun type(mouse: Mouse): Int
fun type(dog: Dog): Int
fun type(car: Car): Int
}
這種方式是完全的類型安全,沒有 instance-of 檢查,也根本不需要轉(zhuǎn)型。
這個(gè)工廠方法的責(zé)任是明確的:它知道所有的 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
我也可以創(chuàng)建 ViewHolder 在某個(gè)地方持有關(guān)于布局 id 的信息。所以當(dāng)添加一個(gè)新 view 的時(shí)候,這個(gè)地方也跟著添加。這是相當(dāng)符合 SOLID 原則的。你可能需要為新的類型創(chuàng)建另一個(gè)方法,但是不修改任何存在的方法:對擴(kuò)展開放,對修改封閉。
現(xiàn)在你可能會問:為什么不直接在 adapter 里使用工廠方法而是間接的使用 model 呢?通過這個(gè)方式你可以不需要轉(zhuǎn)型和類型檢查就可以確保類型安全?;c(diǎn)時(shí)間在這里實(shí)現(xiàn)它,這不是一個(gè)需要的轉(zhuǎn)型!間接引用正是訪問者模式背后的魔法。
通過這個(gè)方法使得 adapter 擁有一個(gè)非常通用的實(shí)現(xiàn),并且?guī)缀醪恍枰兓?/p>
結(jié)論
- 盡力保持你的 presenter 層代碼干凈。
- Instance-of 檢查應(yīng)該是一個(gè)警告標(biāo)志,盡量不要使用!
- 注意向下轉(zhuǎn)型,因?yàn)檫@是壞代碼的味道.
- 盡量把上面兩個(gè)替換成正確的面向?qū)ο笥梅???紤]下接口和繼承。
- 盡量使用通用的方式來避免轉(zhuǎn)型。
- 使用 ViewModel。
- 檢查訪問者模式的使用方式。
我很樂意了解到更多其他的想法來使我們的 adapter 保持整潔。