- 原文作者: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 保持整潔。