[譯]關于 Android Adapter,你的實現方式可能一直都有問題

對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 保持整潔。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容