仿B站Android客戶端系列(二)基于MultiType的RecyclerView.Adapter封裝

上接??????仿B站Android客戶端系列(一)項目環(huán)境搭建

前言

任何項目幾乎都會用RecyclerView作為列表的實現(xiàn)。在傳統(tǒng)的開發(fā)方式中,簡單的單一類型數(shù)據(jù)列表非常容易實現(xiàn),當(dāng)需要支持多種數(shù)據(jù)類型及布局時,我們的代碼往往會堆積在Adapter中,當(dāng)Adapter封裝了一些通用操作時,該類更會顯得臃腫不堪,不便于維護(hù)。好的封裝會大大節(jié)省開發(fā)效率,增強(qiáng)代碼的易讀性,這樣在開發(fā)以及修改的過程中可以節(jié)省不少時間。

在瀏覽b站App時可以看到各個頁面的列表基本都有如下特性:支持下拉刷新、Loading、加載失敗、加載以及存在加載的各種狀態(tài)。這篇文章主要說一下在FakeBiliBili項目的開發(fā)過程中,對于Adapter封裝的思路和一些想法。

MultiType

這里先安利一個解決多類型問題的Adapter庫:

MultiType

這是一個直觀、靈活、可靠、簡單純粹的庫,其中設(shè)計思想非常值得學(xué)習(xí)。

基礎(chǔ)的用法如下:

public class MainActivity extends AppCompatActivity {

    private MultiTypeAdapter adapter;

    /* Items 等同于 ArrayList<Object> */
    private Items items;

    @Override 
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = (RecyclerView) findViewById(R.id.list);
        mAdapter = new MultiTypeAdapter();
        //注冊,數(shù)據(jù)類型對應(yīng)ViewBinder
        mAdapter.register(TextBean.class, new TextViewBinder());
        mAdapter.register(ImageBean.class, new ImageViewBinder());
        mAdapter.register(VideoBean.class, new VideoViewBinder());
        mRecyclerView.setAdapter(mAdapter);
        //設(shè)置列表數(shù)據(jù)
        Items<Object> items = new Items<>();
        items.add(new ImageBean(R.drawable.image1));
        items.add(new ImageBean(R.drawable.image2));
        items.add(new TextBean("text1"));
        items.add(new TextBean("text2"));
        items.add(new TextBean("text3"));
        items.add(new VideoBean(url));
        mAdapter.setItems(items);
        mAdapter.notifyDataSetChanged();
        }
}
//ItemViewBinder示例
public class TextViewBinder extends ItemViewBinder<TextBean, TextViewBinder.ViewHolder> {

    @NonNull @Override
    protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
        View view = inflater.inflate(R.layout.item_text, parent, false);
        return new ViewHolder(view);
    }

    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull TextBean textBean) {
        holder.category.setText(textBean.tv_text);
    }

    static class ViewHolder extends RecyclerView.ViewHolder {

        @NonNull private TextView tv_text;

        ViewHolder(@NonNull View itemView) {
            super(itemView);
            tv_text = (TextView) itemView.findViewById(R.id.tv_text);
        }
    }
}

這個庫的核心是通過類型池TypePool來管理注冊 binder 與 class 來實現(xiàn)不同類型布局的策略模式,MultiType內(nèi)部將復(fù)用這個 binder 對象來生產(chǎn)所有相關(guān)的 item、views 和綁定數(shù)據(jù)。用法就是注冊綁定數(shù)據(jù)類型和 ItemViewBinder,然后向內(nèi)置的數(shù)據(jù)集合對象中添加數(shù)據(jù)然后通知刷新,Item便會根據(jù)集合中的順序依次顯示。

MultiType提供的 ItemViewBinder 沿襲了 RecyclerView.Adapter 的接口命名,很容易理解,這樣我們可以輕松的實現(xiàn)多種類型列表,而且代碼清晰、直觀,方便修改,相比傳統(tǒng)的寫法可以說是省了不少時間和維護(hù)精力。詳細(xì)用法請移步該庫Wiki。

擴(kuò)展

通過MultiType的支持,現(xiàn)在沒有復(fù)雜類型列表的問題了,但MultiType并不能滿足上拉加載、顯示Loading、加載失敗或是需要添加Header、Footer等其他需求,這時候需要我們自己擴(kuò)展來實現(xiàn),下面就來聊聊我的實現(xiàn)思路。

一.關(guān)于加載更多的擴(kuò)展

首先就是封裝加載更多這樣的常用功能,通常的寫法是

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_LOAD_MORE) {
            return ...
        }
        return ...;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (getItemViewType(position) == TYPE_LOAD_MORE) {
            ...      
        } else {
            ...
        }
    }

    @Override
    public int getItemCount() {
        return mDatas.size() + 1;//給出加載更多的位置
    }
    
    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount()) {
            return TYPE_LOAD_MORE;//當(dāng)顯示最后一個Item,使其為加載更多類型
        } else {
            return ...
        }
    } 

那么基本思路就是繼承MultiTypeAdapter并重寫這幾個方法,當(dāng)需要顯示我們定制的通用Item時,在繼承類中實現(xiàn),當(dāng)需要顯示MultiType職責(zé)內(nèi)的Item時,歸還給父類MultiTypeAdapter實現(xiàn)。但是當(dāng)我嘗試?yán)^承MultiTypeAdapter重寫這些方法時,發(fā)現(xiàn)作者給這些方法都加上了final,那么便無法繼承擴(kuò)展。開始不是很理解,還提了issue給作者,得到的回復(fù)是這樣的:

使用 final 意在避免用戶自定義破壞了封裝并且歸結(jié)認(rèn)為是 MultiType 的問題。如果你需要覆寫這些 final 方法,你應(yīng)該考慮采用組合而非繼承,即創(chuàng)建一個 Adapter 包含 MultiTypeAdapter 而不是繼承 MultiTypeAdapter。

MultiTypeAdapter 并不是為繼承而設(shè)計的類?!禘ffective Java》一書中指出:使類和成員的可訪問性最小化,并且要么為繼承而設(shè)計,并提供文檔說明,要么就禁止繼承。在 Kotlin 語言設(shè)計中,也是遵循了這個原則,所有類默認(rèn)都是 final,所有方法默認(rèn)都是 final,除非特意標(biāo)注 open.

看完豁然開朗!其實《Effective Java》也讀過,但平常寫代碼時沒有注意,導(dǎo)致做了很多過度設(shè)計。

所以我需要用一個裝飾模式來完成需求的擴(kuò)展,這里修改一下:

    protected RecyclerView.Adapter innerAdapter;

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_LOAD_MORE) {
            return ...
        }
        return innerAdapter.onCreateViewHolder(parent, viewType);//交給目標(biāo)adapter處理
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (getItemViewType(position) == TYPE_LOAD_MORE) {
            ...
            return;    
        }
        innerAdapter.onBindViewHolder(holder, position);//交給目標(biāo)adapter處理
    }

    @Override
    public int getItemCount() {
        return innerAdapter.getItemCount() + 1;//給出加載更多的位置
    }
    
    @Override
    public int getItemViewType(int position) {
        if (position == getItemCount() - 1) {
            return TYPE_LOAD_MORE;//當(dāng)顯示最后一個Item,使其為加載更多類型
        } 
         innerAdapter.getItemViewType(position);//交給目標(biāo)adapter處理
    } 

這也就是很多開源庫都會用到的裝飾模式寫法,比如鴻陽的baseAdapter,可惜有很多bug并且不維護(hù)了。

二.完善

基礎(chǔ)的骨架有了,接下來就是進(jìn)一步完善這個adapterWraaper,這里說一下寫代碼過程中值得注意的地方和一些坑。

1.兼容GridLayoutManager和StaggeredGridLayoutManager

當(dāng)使用這兩種LayoutManager時,需要對我們自己實現(xiàn)的ViewType在不同LayoutManager時做一些特殊處理,否則當(dāng)列數(shù)大于1時,加載更多item便不能撐滿一行。

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        recyclerView.addOnScrollListener(mOnScrollListener);
        if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
            final GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
            final GridLayoutManager.SpanSizeLookup oldSpanSizeLookup = layoutManager.getSpanSizeLookup();
            layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    if (getItemViewType(position) == ITEM_TYPE_LOAD_MORE) {
                        return layoutManager.getSpanCount();
                    } else {
                        return oldSpanSizeLookup.getSpanSize(position);
                    }
                }
            });
        }
        innerAdapter.onAttachedToRecyclerView(recyclerView);
    }
    
    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        if (isBaseViewHolder(holder)) {
            innerAdapter.onViewAttachedToWindow(holder);
        } else {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
            if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
                p.setFullSpan(true);
            }
        }
    }

2. ItemDecoration與SpanSizeLookup

由于我們運(yùn)用裝飾模式的處理了GridLayoutManager時帶來的跨度問題,但此時作用于GridLayoutManager中的SpanSizeLookup是我們的裝飾類,這可能會引發(fā)一些問題。例如我有時會在ItemDecoration中直接把SpanSizeLookup對象當(dāng)做參數(shù)傳進(jìn)來,用于判斷該項的跨度。

public class CustomItemDecoration extends RecyclerView.ItemDecoration {

    private GridLayoutManager.SpanSizeLookup spanSizeLookup;

    public CustomItemDecoration(GridLayoutManager.SpanSizeLookup spanSizeLookup) {
        this.spanSizeLookup = spanSizeLookup;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        ...
        int spanSize = spanSizeLookup.getSpanSize(position);
        ...
    }

這樣顯然會出現(xiàn)意料之外的問題,這時候我們不能直接把SpanSizeLookup傳進(jìn)來,而是通過recyclerView對象獲得,像這樣:

@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        ...
        GridLayoutManager.SpanSizeLookup spanSizeLookup = ((GridLayoutManager) parent.getLayoutManager()).getSpanSizeLookup();
        int spanSize = spanSizeLookup.getSpanSize(position);
        ...
    }

3.不要忘記其他重寫方法的處理

詳細(xì)代碼見該項目中

DefaultAdapterWrapper.java

三.效果

加載更多
加載更多失敗
點擊重試
加載失敗
Loading

四.其他

1.Header和Footer

關(guān)于Header和Footer的問題,MultiType作者給出了解決方案:

MultiType 其實本身就支持 HeaderView、FooterView,只要創(chuàng)建一個 Header.class - HeaderViewBinder 和 Footer.class - FooterViewBinder 即可,然后把 new Header() 添加到 items 第一個位置,把 new Footer() 添加到 items 最后一個位置。需要注意的是,如果使用了 Footer View,在底部插入數(shù)據(jù)的時候,需要添加到 最后位置 - 1,即倒二個位置,或者把 Footer remove 掉,再添加數(shù)據(jù),最后再插入一個新的 Footer.

最后

僅僅是個人理解,不合理和不完善的地方還請留言指教,謝謝!

項目地址:FakeBiliBili

還可以的話就賞個star吧!(≧▽≦)/

最后編輯于
?著作權(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)容