Android復(fù)雜列表的實現(xiàn)

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

初步實現(xiàn)

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

@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;
        }
    }

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

@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中實現(xiàn)多類型Item的方法,相應(yīng)的變化一下,把頭部和腳部當(dāng)作特定類型的ItemType,并提供public方法共外部setHead即可支持添加頭部。

問題進(jìn)階

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

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

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

1.jpg
2.jpg

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

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

3.jpg

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

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中去編寫業(yè)務(wù)邏輯就可以,Adapter中的職業(yè)就非常簡單,只需要持有AdapterDelegateManager,由這個Manager類來維護(hù)每種類型具體對應(yīng)的AdapterDelegate,而由AdapterDelegate維護(hù)UI和數(shù)據(jù)的綁定關(guān)系。

4.jpg

如此,面對多類型的情況或者在已有的業(yè)務(wù)基礎(chǔ)上增加了新的類型,都不再用去修改Adapter的基本實現(xiàn),只要做兩件事:

  • 編寫類型的AdapterDelegate來實現(xiàn)UI展示、數(shù)據(jù)綁定、點擊事件等工作
  • 通過AdapterDelegateManager注冊新的AdapterDelegate

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

5.jpg

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

    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是非常簡潔和清晰的,業(yè)務(wù)邏輯歸于Delegate當(dāng)中解決,viewType和類型的映射關(guān)系放到delegateManager中處理。具體Delegate的代碼就不貼了,和常規(guī)單類型Adapter的寫法一致。下面再看看另一個庫的思路:MuliTypeAdapter.
這里就不自己畫類圖了,從其作者的文檔中引用一幅圖,如下:

6.jpg

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

  • TypePool
  • FlatTypeAdapter

因此,維護(hù)viewType和類型映射關(guān)系就必然會體現(xiàn)在其中。而類Items是一個繼承ArrayList<Object>的空類,表明了這個類將是所有數(shù)據(jù)結(jié)構(gòu)的基類。最后,唯一單獨沒有聯(lián)系的ItemViewProvider<C,V>則可以推斷為用來實現(xiàn)業(yè)務(wù)邏輯和UI展示。如此,基本要素都一一對應(yīng)上,接下來看看它是如何實現(xiàn)其中的功能。

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的幾個重點方法可以看出,其調(diào)用的方法幾乎都是接口或者抽象類的空方法,這側(cè)面體現(xiàn)出來此庫的高度可定制性,所有的方法實現(xiàn)都可以由具體的實現(xiàn)類來決定。

從getViewType方法中可以看到,其返回值由indexOf方法確定,而這個方法定義在TypePool接口中,由MultiTypePool實現(xiàn),當(dāng)然我們也可以自己實現(xiàn)然后替換掉。從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,在這里,它將數(shù)據(jù)類Class和ItemViewProvider進(jìn)行了綁定,分別用兩個ArrayList來存儲對象,用index索引作為viewType的值。如下圖示意:

7.jpg

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

而在編寫Adapter的過程中,特別是多類型的Adapter過程中,常常會發(fā)現(xiàn)自己不得不在onBindVieHolder方法中,對holder轉(zhuǎn)型來調(diào)用其內(nèi)部方法,或者對數(shù)據(jù)轉(zhuǎn)型來使用其字段值,大量的類型轉(zhuǎn)換既顯得臃腫又影響速度。既然我們已經(jīng)把不同類型的情況已經(jīng)獨立成一個個ItemViewProvider(或者AdapterDelegate,另一個庫中的稱呼),那么在相應(yīng)的實現(xiàn)類中,我們也希望能正確的分發(fā)數(shù)據(jù)類型和視圖類型。
在AdatperDelegates庫中,如果我們的業(yè)務(wù)實現(xiàn)類直接繼承與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());
   }
}

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

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的抽象方法時就已經(jīng)考慮了這個問題,解決方案和上述一致,但是寫法上看起來更為優(yōu)雅:

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

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

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

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

參考文章:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容