Writing Better Adapters

weixin_34185560發表於2017-02-12
4064751-a6796b4541d4b020.jpg

實現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

相關文章