Android復雜列表的實現

RecyclerView控件從2014發布以來,目前已經普遍用于項目中,來承載各種列表內容。同時,列表樣式也隨著項目變的越來越復雜,從簡單統一的列表,變化成頭部、腳部、不同類型的Item互相組合。本文將通過一些開源庫來學習一下如何實現各種復雜類型的列表,分析了viewType應該如何與視圖、數據相綁定,并將業務邏輯單獨分離。

初步實現

問題的開始是這樣的:項目里有個頁面,整個列表采用ListView實現,除了常規的列表項外,還有兩個自定義的View也要隨著頁面滑動。Ok,listView支持addHead,而且還是多head,自定義view通過addHead方法添加到listview中,就一切ok。然而ListView畢竟漸漸過時了,打算采用RecyclerView來重構一下。雖然RecyclerView不支持addHead這種方法,但是可以通過getItemViewType方法來實現返回多種類型。

@Override
public int getItemViewType(int position) {
    switch (position) {
        case 0:
            return TYPE_HEAD1;
        case 1:
            return TYPE_HEAD2;
        case 2:
            return TYPE_ITEM;
        default:
            return TYPE_ITEM;
        }
    }

即根據業務需求,返回不同的類型的值,那么下一步,我們同時需要在onCreateViewHolder中針對不同的viewType來創建不同的ViewHolder,同樣的,在onBindViewHolder中,也要處理不同的類型,特別的,如果不同類型的viewholder具有不同的方法的情況,還需要針對viewholder做一次類型轉換。類似這樣:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (getItemViewType(position) == TYPE_HEAD1) {
        ((Head1VH) holder).bindData();
    } else if (getItemViewType(position) == TYPE_HEAD2) {
        ((Head2VH) holder).bindData();
    } else if (getItemViewType(position) == TYPE_ITEM) {
        ((Item) holder).bindData();
    }
}

以上就是一般RecyclerView中實現多類型Item的方法,相應的變化一下,把頭部和腳部當作特定類型的ItemType,并提供public方法共外部setHead即可支持添加頭部。

問題進階

上述的方法,是解決了特定業務情景下的問題,但是很明細不利于擴展和維護。首先,當列表除了頭部外的部分依然會出現不同類型時,并且實際情況中,不同類型應該都是由服務器回傳的數據來決定的,我們就不能在getItemViewType中簡單的定義類型值來判斷。
一個可能的做法是,在數據層里添加type字段,通過type字段來

@Override
public int getItemViewType(int position) {
    return datas.get(position).type;
} 

然而在數據層包裹展示層需要的type字段并不是一個優雅的做法,它破壞了單一職責。同時,這么做也無法解決另一個問題:擴展性。
所謂擴展性就是Adapter最好能在數據類型變化時候,內部實現邏輯不需要改變,只是外部添加新的功能即可。那么這就要求Adapter對數據層是解耦的,不能顯式的持有外部的數據。Adapter設計之初,是為了兼容千變萬化的數據結構,并不是千變萬化的類型結構,因此,應該考慮把不同類型的變化從Adapter內部隔離開。

1.jpg
2.jpg

GitHub上關于多類型Item的RecyclerView的實現有很多庫,基本的思路是通過一個Manager類來管理多種類型中:數據和視圖的對應關系。實際上,都是圍繞如何解決viewType、數據、視圖的對應關系來進行一系列的封裝。
下面介紹兩個實現的比較簡潔而靈活的庫:

AdapterDelegates的思路是使用自定義的Adapter來“hook”原來的RecyclerView的Adapter,主要的Adapter方法如onBindViewHolder和onCreateViewHolder方法都被劫持使用adpter內部的一個Manager類來實現,參看下面的類圖會更加容易理解。

3.jpg

上圖是這個庫的基本類圖,省略了兩個非必要的類,其中只列出了一些典型的方法和對象。以onBindViewHolder()為例,可以看到從最頂層開始,這個方法會一步步往下調用,一直到AdapterDelegate這層,這一層也是最終面向使用者需要關心的層次,通過繼承抽象類AdapterDelegate,實現其中的方法,來完成業務邏輯和UI表現,代碼如下,和普通的RV.Adapter方法沒有區別:

public class NormalDelegate extends AbsListItemAdapterDelegate<NormalItem, Item, NormalDelegate.NormalItemVH> {


    @NonNull
    @Override
    protected NormalItemVH onCreateViewHolder(@NonNull ViewGroup parent) {
        return new NormalItemVH(inflater.inflate(R.layout.normal_item, parent, false));
    }

    @Override
    protected void onBindViewHolder(@NonNull NormalItem item, @NonNull final NormalItemVH viewHolder, @NonNull List<Object> payloads) {
        viewHolder.imageView.setImageResource(item.resId);
        viewHolder.imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                DetailsActivity.startActivity(view.getContext());
            }
        });
        viewHolder.textView.setText(item.content);
        viewHolder.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String old = viewHolder.textView.getText().toString();
                viewHolder.textView.setText(old + " " + (int) (10 * Math.random()));

            }
        });
    }
}

但是通過這一層的封裝,成功的把多類型的情況分隔開,每種類型只需要在各種的AdapterDelegate中去編寫業務邏輯就可以,Adapter中的職業就非常簡單,只需要持有AdapterDelegateManager,由這個Manager類來維護每種類型具體對應的AdapterDelegate,而由AdapterDelegate維護UI和數據的綁定關系。

4.jpg

如此,面對多類型的情況或者在已有的業務基礎上增加了新的類型,都不再用去修改Adapter的基本實現,只要做兩件事:

  • 編寫類型的AdapterDelegate來實現UI展示、數據綁定、點擊事件等工作
  • 通過AdapterDelegateManager注冊新的AdapterDelegate

下面是一個demo例子(gif畫質比較渣,將就著看。。)

5.jpg

整個列表是一個RecyclerView,包含了兩種不同類型的頭部,簡單的Item類型和可橫向滑動展示的Item類型共計4種。來看看這個RecyclerView的Adapter實現:

    class ItemList2Adapter extends ListDelegationAdapter<List<Item>> {
        Activity activity;
        List<Item> datas;

        public ItemList2Adapter(Activity activity, List<Item> datas) {
            this.activity = activity;
            this.datas = datas;
            delegatesManager.addDelegate(new Head1Delegate(activity))
                    .addDelegate(new Head2Delegate(activity))
                    .addDelegate(new NormalDelegate(activity))
                    .addDelegate(new HorizontalItemDelegate(activity));
            setItems(datas);
        }
    }

從代碼里可以看到,整個Adapter是非常簡潔和清晰的,業務邏輯歸于Delegate當中解決,viewType和類型的映射關系放到delegateManager中處理。具體Delegate的代碼就不貼了,和常規單類型Adapter的寫法一致。下面再看看另一個庫的思路:MuliTypeAdapter.
這里就不自己畫類圖了,從其作者的文檔中引用一幅圖,如下:

6.jpg

從上文所說的基本原則來分析,我們應重點關注其如何實現viewType字段和類型的映射,以及如何和RV.Adaper交互。從類名和繼承關系來看,我們可以知道,MultiTypeAdapter應該是充當之前所說的Manage的角色,同時,這個類實現了兩個接口:

  • TypePool
  • FlatTypeAdapter

因此,維護viewType和類型映射關系就必然會體現在其中。而類Items是一個繼承ArrayList<Object>的空類,表明了這個類將是所有數據結構的基類。最后,唯一單獨沒有聯系的ItemViewProvider<C,V>則可以推斷為用來實現業務邏輯和UI展示。如此,基本要素都一一對應上,接下來看看它是如何實現其中的功能。

public class MultiTypeAdapter extends RecyclerView.Adapter<ViewHolder>{
    @Override
    public int getItemViewType(int position) {
        Object item = items.get(position);
        return indexOf(flattenClass(item));
    }


    @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int indexViewType) {
        if (inflater == null) {
            inflater = LayoutInflater.from(parent.getContext());
        }
        ItemViewProvider provider = getProviderByIndex(indexViewType);
        provider.adapter = MultiTypeAdapter.this;
        return provider.onCreateViewHolder(inflater, parent);
    }


    @SuppressWarnings("unchecked") @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        Object item = items.get(position);
        ItemViewProvider provider = getProviderByClass(flattenClass(item));
        provider.onBindViewHolder(holder, flattenItem(item));
    }
}

從MuliTypeAdapter的幾個重點方法可以看出,其調用的方法幾乎都是接口或者抽象類的空方法,這側面體現出來此庫的高度可定制性,所有的方法實現都可以由具體的實現類來決定。

從getViewType方法中可以看到,其返回值由indexOf方法確定,而這個方法定義在TypePool接口中,由MultiTypePool實現,當然我們也可以自己實現然后替換掉。從MultiTypePool的源碼中分析:

    private ArrayList<Class<?>> contents;
    private ArrayList<ItemViewProvider> providers;
    
    public void register(Class<?> clazz,ItemViewProvider provider) {
        if (!contents.contains(clazz)) {
            contents.add(clazz);
            providers.add(provider);
        } else {
            int index = contents.indexOf(clazz);
            providers.set(index, provider);
            Log.w(TAG, "You have registered the " + clazz.getSimpleName() + " type. " +
                "It will override the original provider.");
        }
    }

    @Override
    public int indexOf(Class<?> clazz) {
        int index = contents.indexOf(clazz);
        if (index >= 0) {
            return index;
        }
        for (int i = 0; i < contents.size(); i++) {
            if (contents.get(i).isAssignableFrom(clazz)) {
                return i;
            }
        }
        return index;
    }

可以看到,不同于AdapteDelegate中綁定viewType和Delegate,在這里,它將數據類Class和ItemViewProvider進行了綁定,分別用兩個ArrayList來存儲對象,用index索引作為viewType的值。如下圖示意:

7.jpg

當Adapter中注冊類型時,將兩者綁定;getViewType時,則首先通過position拿到數據類型,再通過數據類型拿到對應的UI類型;onBindViewHolder時,同樣通過position拿到數據類型,拿到ItemViewProvider,繼而調用ItemViewProvider的onBindViewHolder方法去交由實現類處理。以上應該可以基本明白該庫是如何維護viewType、數據類型和UI類型的映射關系的。

而在編寫Adapter的過程中,特別是多類型的Adapter過程中,常常會發現自己不得不在onBindVieHolder方法中,對holder轉型來調用其內部方法,或者對數據轉型來使用其字段值,大量的類型轉換既顯得臃腫又影響速度。既然我們已經把不同類型的情況已經獨立成一個個ItemViewProvider(或者AdapterDelegate,另一個庫中的稱呼),那么在相應的實現類中,我們也希望能正確的分發數據類型和視圖類型。
在AdatperDelegates庫中,如果我們的業務實現類直接繼承與AdapterDelegate來編寫,是這樣的:

public class Head1Delegate extends AdapterDelegate<List<Item>> {

@Override
protected void onBindViewHolder(@NonNull List<Item> items, 
int position, @NonNull RecyclerView.ViewHolder holder, 
@NonNull List<Object> payloads) {
   
((Head1VH) holder).imageView.
setImageResource(((Head1) items.get(position)).getResId());
   }
}

可以看到還是沒有避免類型轉換。作者其實也意識到這點,因此提供了一個AbsListItemAdapterDelegate類來供我們繼承,其內部通過泛型預先幫我們做好類型轉換,再分發下去:

public abstract class AbsListItemAdapterDelegate<I extends T, T, 
VH extends RecyclerView.ViewHolder>
   extends AdapterDelegate<List<T>> {
   
@Override 
protected final void onBindViewHolder(@NonNull List<T> items, int position,
     @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
     
   onBindViewHolder((I) items.get(position), (VH) holder, payloads);
 }

MuliTypeAdapter則干脆的多,在定義ItemViewProvider的抽象方法時就已經考慮了這個問題,解決方案和上述一致,但是寫法上看起來更為優雅:

  protected abstract void onBindViewHolder(@NonNull V holder, @NonNull T t);

當然,這樣做本質是在內層做好轉型再分發,如果要真正意思上的避免轉型,可以采用訪問者模式(參見:Writing Better Adapter)

關于MuliTypeAdapter的Demo就不做了,其官方上例子已經很詳盡。并且,除了之前提到的核心邏輯外,其還提供了全局類型池設計、數據二次分發設計(即沒有討論的FlatTypeAdapter接口),感興趣的可以繼續了解。

上述兩個庫,都做到了對不同類型Item的分離,每次組裝一個列表時,只需要把數據源正確的組裝好,adapter內部會通過各自實現的Manager來定位對應的UI來展示。在實際開發中,可能的問題或許是不同Item之間的關聯性,比如一個頭部類型的帶有聯動其他Item的交互的話,就需要打破這種獨立性(此時需要通過構造函數等方法傳入其他對象的實例)。另外,對于常見的頭部、列表、腳部的需求來說,實際上在此都是當作三種類型來處理,那么對于服務器回傳的列表數據,我們需要自行包裹上頭部、腳部的數據類型,這樣才能正確的被處理,也是相對麻煩之處。

參考文章:

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

推薦閱讀更多精彩內容