Writing-Better-Adapters 譯文及示例
對于 Android 開發者來說, 實現 Adapter 是最頻繁的工作之一. Adapter 是所有列表的基本, 而列表也是很多 App 的基本組成.
編寫一個列表控件的方法大多數時間都是一樣的: 用一個綁定了 Adapter 的 View 來展示數據. 然而一直這樣會讓我們對自己編寫的代碼變得盲目, 盡管那是辣雞代碼. 或者說, 我們一直在重復創造辣雞代碼.
讓我們深入地看看 Adapter 的代碼.
RecyclerView 的基礎
RecyclerView(同樣適用于 ListView) 的基礎操作:
-
創建 View 和儲存 View 信息的 ViewHolder.
-
根據 ViewHolder 儲存的信息來綁定 ViewHolder , 大部分是 List 中的 model.
這些步驟都比較簡單, 一般不會出錯.
包含多種類型的 RecyclerView
當 View 需要展示多重不同的類型時, 事情就變得麻煩了. 比如下面這個例子.
interface Animal {}
class Mouse implements Animal {}
class Duck implements Animal {}
class Dog implements Animal {}
class Car
這個例子中,我們需要處理不同類型的動物, 而另一個對象”車”又與動物無關. 這意味著你需要創建不同的 ViewHolder 并對每一個 ViewHolder 初始化不同的布局. API 對每個類型定義了不同的 int 對象, 這就是辣雞代碼的開始.
讓我們看看一些常見的代碼, 當你需要多個類型時, 需要重寫這個方法:
@Override
public int getItemViewType(int position) {
return 0;
}
默認實現返回0, 但多種類型時需要根據類型返回不同的值.
下一步,創建 ViewHolder, 即實現這個方法:
@Override
public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
}
根據上一步 getItemViewType() 返回的不同 viewType 值來創建不同的 ViewHolder, 方法可能是 switch 語句或 if-else語句, 不過這個關系不大.
同樣的, 在綁定這個創建了的(或者回收了的) ViewHolder 時, 也需要處理不同類型.
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
}
因為這里沒有 type 參數, 所以會根據 ViewHolder 的 Instance-of 判斷不同類型, 或者也可以在這些 ViewHolder 的基類中處理 onBind .
不好的地方
所以上面這種實現有什么問題呢? 看起來不是很直觀嗎?
讓我們再看一下 getItemViewType() 方法: 系統需要知道每個位置的類型, 所以你需要將你的數據列表中的每一項都轉化成視圖類型. 那就可能產生這種代碼:
if (things.get(position) == Duck) {
return TYPE_DUCK;
} else if (things.get(position) == Mouse) {
return TYPE_MOUSE;
}
你現在覺得這個代碼糟糕嗎? 如果不覺得, 那我們接下來看看 onBindViewHolder() 的實現.
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
Thing thing = things.get(position);
if (thing == Animal) {
((AnimalViewHolder) thing).bind((Animal) thing);
} else if (thing == Car) {
((CarViewHolder) thing).bind((Car) thing);
}
}
這段代碼看起來就很亂了, Instance-of 的檢查和強制類型轉化使這段代碼非常違背設計模式.
許多年前我的顯示器上就貼著一段引用自 Scott Meyers 所寫的 Effective C++ 中的一段話:
Anytime you find yourself writing code of the form “if the object is of type T1, then do something, but if it’s of type T2, then do something else,” slap yourself.
每次你發現自己寫的代碼是像”如果這個對象是 T1 類型, 就做這個, 或者如果這個對象是 T2 類型, 就做那個”的樣式的話, 就扇自己一巴掌
所以當然回頭看 Adapter 的實現時, 就需要扇自己很多下.
-
我們有很多類型檢查和強制類型轉化.
-
這是明顯的適合使用面向對象卻沒有使用的代碼.
-
實現方式違背了 SOLID 原則中的 開閉原則 , 即拓展時不需要修改內部實現.
嘗試解決
一種替代方式就是在過程中間加上轉化步驟, 比如一種很簡單的方式就是把所有類的類型都放在一個 map 里面, 通過一次調用獲取即可.
@Override
public int getItemViewType(int position) {
return types.get(thing.class);
}
這樣做會好點嗎?
不幸的是, 這不完全解決問題, 這種方式只是隱藏了 Instance-of 的檢查.因為接下來實現 onBindViewHolder() 時還是會產生 如果這個對象是 T1 類型, 就做這個, 或者如果這個對象是 T2 類型, 就做那個 這樣的代碼.
我們的目標應該是 添加新的類型時不需要修改 Adapter 的代碼 .
因此, 一開始不需要在 Adapter 中創建視圖和數據的對應關系. 另外 Google 也推薦使用布局 id 來區分不同類型, 這樣你就不用自己定義每種類型了.
從另一個角度想, 如果我們想創建視圖和數據的對應關系, 不一定非要從數據集入手, 可以對每個數據添加一個方法:
public int getType() {
return R.layout.item_duck;
}
那 Adapter 中獲取類型的方法就可以變成
@Override
public int getItemViewType(int position) {
return things[position].getType();
}
這樣就符合了開閉原則, 當我們想要添加新類型時不需要再改變 Adapter 中的代碼.
但這種做法把架構中的每個層打亂了. 實體類型知道它的表現形式, 這種關系指向是錯誤的. 對我們來說不可接受. 另一方面, 在數據中添加方法來表示它的類型, 這種做法并不是面向對象的, 我們只是再一次隱藏了 Instance-of 的檢查.
ViewModel 視圖模型
更進一步的處理方法, 便是使用獨立的視圖模型而不是直接使用模型. 歸根結底, 我們的數據是不想交的, 他們沒有一個相同的基類: 車不是動物. 這對數據層來說是對的, 但對表現層來說, 他們都是展現在同一個視圖中, 所以它們可以有一個共同的基類.
abstract class ViewModel {
abstract int type();
}
class DuckViewModel extends ViewModel {
@Override
int type() {
return R.layout.duck;
}
}
class CarViewModel extends ViewModel {
@Override
int type() {
return R.layout.car;
}
}
這樣就簡單封裝了數據. 當添加新的視圖類型時, 不需要改動 Adapter 和原來的視圖類型的代碼. 比如 RecyclerView 的其他類型: 分割線, 段落頭部, 廣告等.
這只是一個比較接近的解決方法, 但并不唯一.
The Visitor 訪問者模式
如果你有很多模型類 (Model class) , 可能你不會向對每一個再創建對應的視圖模型類 (ViewModel class). 那我們再來看看如何只使用數據模型 (model) .
一開始的時候, 當我們把 type() 方法加到每個模型中, 這種方法就太耦合了. 我們應該把方法抽象出來, 比如添加一個接口:
interface Visitable {
int type(TypeFactory typeFactory);
}
那么每個模型就會變成這樣:
interface Animal extends Visitable {
}
class Mouse implements Visitable {
@Override
int type(TypeFactory typeFactory) {
return typeFactory.type(this);
}
}
class Car implements Visitable {
@Override
int type(TypeFactory typeFactory) {
return typeFactory.type(this);
}
}
而工廠類也應該是個抽象類, 它包含了所需的類型.
interface TypeFactory {
int type(Duck duck);
int type(Mouse mouse);
int type(Dog dog);
int type(Car car);
}
這樣就完全是類型安全的實現, 不需要 Instance-of 的判斷, 也不需要強制類型轉化. 對于每個具體工廠的實現也是清晰的, 實現對應的類型即可.
class TypeFactoryForList extends TypeFactory {
@Override
public int type(Duck duck) {
return R.layout.duck;
}
@Override
public int type(Mouse mouse) {
return R.layout.mouse;
}
@Override
public int type(Dog dog) {
return R.layout.dog;
}
@Override
public int type(Car car) {
return R.layout.car;
}
}
當我們還想添加新的類型時, 這是我們需要添加代碼的地方. 這就非常符合 SOLID 原則. 你可能需要別的方法給新的類型, 但不需要修改任何已經存在的方法: 對拓展打開, 對修改關閉.
現在你可能會問: 為什么不直接在 Adapter 使用工廠類, 而是使用抽象工廠呢?
因為只有這樣才能保證類型安全, 避免類型轉化或類型檢查. 花點時間認真想想這里, 這里沒有用到任何一個轉化. 這就是訪問者模式帶來的間接性.
根據以上這些步驟能保證 Adapter 非常通用.
結論
-
嘗試保持讓你的實現代碼簡潔.
-
Instance-of 檢查應該是一個紅色的警告標志.
-
注意向下轉化, 這是不好的代碼的味道.
-
嘗試讓上面那個點轉化為正確的面向對象的用法, 想想接口和繼承.
-
嘗試用通用的點來阻止轉化.
-
使用 ViewModel .
-
注意訪問者模式的使用.
我很樂于去學習更多讓 Adapter 簡潔的方法.
其實這篇文章著重介紹的是優化的思路, 代碼實現也只是給了一些小模塊, 并不是完整的 Adapter 實現.
我沿著這個思路, 寫了一個 Java 版本的 Demo :
BetterAdapter . 歡迎各位指導并提出寶貴的修改意見(我也認為這個實現還是有改進空間).
在實現的過程中, 發現了一些需要注意的地方.
-
由于每個 Adapter 中可能 model 和 type 的對應不同, 比如同一個 model M , 在 Adapter a1 中對應 type1, 在 Adapter a2 中對應 type2, 所以 onCreateViewHolder 的實現也需要放在 TypeFactory 中解耦.
-
為了解耦(同時也是因為想不到好一點的寫法), 我抽出了一個 BetterHolder 作為 ViewHolder 的基類. 然后為每個類型創建對應的 holder 來實現不同的 onCreateViewHolder 和 onBindViewHolder.
public abstract class BetterViewHolder extends RecyclerView.ViewHolder { public BetterViewHolder(View itemView) { super(itemView); } public abstract BetterViewHolder onCreateViewHolder(ViewGroup parent); public abstract void onBindViewHolder(BetterViewHolder holder);
-
所有的數據必須以平級的方式添加進去, 并且需要按照順序添加. 如果后臺 json 數據返回的格式如下:
{ key_a : A key_b : { A, A } key_c : C }
那么在添加的時候:
mAdapter.add(A);
//把 key_b 數組遍歷
mAdapter.add(A);
mAdapter.add(A);
mAdapter.add(C);
來自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/1125/6806.html