OneAdapter: RecyclerView最簡單的萬能適配器

之前寫過一篇使用RecyclerView,一句代碼就夠了,介紹了一個功能較完善的RecyclerView框架的實現(xiàn)。該框架雖然代碼不多,但是仍然不夠簡潔,耦合度也比較高,難以擴展。現(xiàn)將里面的核心部分 OneAdapter 抽取出來,去掉不必要的泛型、類型判斷和其他方法,以實現(xiàn)最簡單、通用性和擴展性最好的Adapter。

ComplexList.png

在Github上搜索adatper,選Java語言,有5K+的記錄,主要也都是RecyclerView或ListView的適配器封裝。既然已經(jīng)有這么多實現(xiàn)在先,這里再實現(xiàn)一遍有意義嗎?

有的,這里的實現(xiàn)是最簡單、代碼最少的。

OneAdapter代碼如下:

/**
 * A custom adapter, supports multi-ItemViewType
 * 
 * Created by rome753 on 2018/2/1.
 */
public class OneAdapter extends RecyclerView.Adapter<OneViewHolder> {

    private final List<Object> data;
    private final List<OneListener> listeners;

    public OneAdapter(OneListener... listeners) {
        this.data = new ArrayList<>();
        this.listeners = new ArrayList<>();
        this.listeners.addAll(Arrays.asList(listeners));
    }

    public void setData(List<?> data) {
        this.data.clear();
        this.data.addAll(data);
    }

    public void addData(List<?> data) {
        this.data.addAll(data);
    }

    public List<Object> getData() {
        return data;
    }

    public List<OneListener> getListeners() {
        return listeners;
    }

    @Override
    public int getItemViewType(int position) {
        Object o = data.get(position);
        for (int i = 0; i < listeners.size(); i++) {
            OneListener listener = listeners.get(i);
            if (listener.isMyItemViewType(position, o)) {
                return i;
            }
        }
        return 0;
    }

    @Override
    public OneViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return listeners.get(viewType).getMyViewHolder(parent);
    }

    @Override
    public void onBindViewHolder(OneViewHolder holder, int position) {
        Object o = data.get(position);
        holder.bindView(position, o);
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

}

OneAdatper繼承自RecyclerView.Adapter,重寫了4個方法,并在其中增加了兩個List,核心代碼只有幾行,在 getItemViewType(int position) 這個方法中。

原因

很多同學(xué)開發(fā)時看到有列表就來一個RecyclerView,然后又實現(xiàn)一個Adapter。這兩步都是沒有必要的。

先說第一步,RecyclerView并不是有列表就使用的。Recycle的意思是回收,也就是說,只有在需要回收時才使用。什么時候需要回收呢?列表數(shù)據(jù)項很多或者單個數(shù)據(jù)項占內(nèi)存很大時。其他情況下,比如類似微信的設(shè)置頁面那種簡單的列表,不需要回收,用ScrollView實現(xiàn)就可以了,代碼更簡單,性能更好。這應(yīng)該也是Google讓開發(fā)者從ListView遷移到RecyclerView的目的。

再說第二步,每個RecyclerView實現(xiàn)一個Adapter也是冗余的。Adapter的本質(zhì)是控制列表中每一項的視圖(View)與數(shù)據(jù)(Data)的對應(yīng)關(guān)系,所以它應(yīng)該只做一件事:RecyclerView把某一項視圖傳過來時,Adapter把數(shù)據(jù)傳給視圖。然而現(xiàn)在Adapter中處理了數(shù)據(jù)類型和視圖類型,這導(dǎo)致它跟具體業(yè)務(wù)耦合度很高,尤其是數(shù)據(jù)類型和視圖類型多樣時。

舉個例子:

    class MyAdapter extends BaseAdapter<SkuItem, BaseHolder<BaseView>> {

        private final int TYPE_HEADER = 0;
        private final int TYPE_COINS = 1;

        @Override
        public int getItemCount() {
            return mData.size();
        }

        @Override
        public BaseHolder<BaseView> onCreateViewHolder(ViewGroup parent, int viewType) {
            if (viewType == TYPE_HEADER) {
                return new BaseHolder(new CoinNumberView(parent.getContext()));
            } else {
                return new BaseHolder(new BuyCoinsView(parent.getContext(), BuyCoinActivity.this));
            }
        }

        @Override
        public void onBindViewHolder(BaseHolder<BaseView> holder, int position) {
            if (holder.itemView instanceof BuyCoinsView) {
                BuyCoinsView buyCoinsView = (BuyCoinsView) holder.bindView;
                buyCoinsView.bindDataByPosition(mData.get(position), position);
            } else if (holder.itemView instanceof CoinNumberView) {
                CoinNumberView coinNumberView = (CoinNumberView) holder.bindView;
                coinNumberView.bindData(null);
            }
        }

        @Override
        public int getItemViewType(int position) {
            if (position == 0) {
                return TYPE_HEADER;
            } else {
                return TYPE_COINS;
            }
        }
    }

這里為了給列表增加一個Header,在Adapter中增加一個類型,然后不得不用 if...else... 或者 switch 語句判斷ItemViewType的類型、ViewHolder的類型和ItemView的類型。

Adapter依賴所有類型的所有對象,畫圖來看是這樣的:

adapter.png

這里只是兩種類型,如果有4,5種乃至7,8種,那么Adapter就爆炸了!

原理

OneAdapter解決了Adapter的過度耦合問題,它只依賴OneListener和OneViewHolder這兩個類,只關(guān)聯(lián)List<OneListener>這一個對象,其他所有依賴關(guān)系都被List<OneListener>轉(zhuǎn)移到外部了。如圖所示:

oneadapter.png

無論有多少種數(shù)據(jù)類型,都只需要在外部實現(xiàn)OneListener和OneViewHolder,給OneAdapter傳入OneListener列表即可。

OneAdapter不依賴具體的數(shù)據(jù)類型,使用Object表示數(shù)據(jù)類型,而不是泛型。這樣做是因為泛型一般針對一種或固定幾種不確定的類型,而Adapter中不但有多種不確定的類型、而且具體有幾種也是不固定的,因此無法使用泛型。為了傳入數(shù)據(jù)不限制于Object類型,在OneAdapter中的 setData(List<?> data) 方法參數(shù)使用了泛型的不確定類型。

OneListener代碼如下:

/**
 * A listener for: define item view type and create ViewHolder, outside of the adapter
 */
public interface OneListener{

    /**
     * Is the position or the data suits for this OneListener?
     * @param position the data's position int the list
     * @param o the data
     * @return true/false
     */
    boolean isMyItemViewType(int position, Object o);

    /**
     * Create a ViewHolder for this OneListener
     * @param parent RecyclerView
     * @return OneViewHolder
     */
    OneViewHolder getMyViewHolder(ViewGroup parent);
}

OneListener是一個接口,它建立了列表中具體位置、具體數(shù)據(jù)與具體OneViewHolder的對應(yīng)關(guān)系。實際上每個OneListener實例表示列表中一種條目類型。它里面有兩個方法。

  • isMyItemViewType(int position, Object o) 方法讓實現(xiàn)者根據(jù)位置或者該位置的數(shù)據(jù)判斷是不是當(dāng)前OneListener對應(yīng)的條目類型。
  • getMyViewHolder(ViewGroup parent) 方法讓實現(xiàn)者實現(xiàn)當(dāng)前OneListener對應(yīng)的OneViewHolder子類。

OneListener也不依賴具體的數(shù)據(jù)類型,因為判斷條目類型并不一定是根據(jù)數(shù)據(jù)類型判斷,也可能根據(jù)位置判斷。這給了調(diào)用者最大的靈活度。雖然OneViewHolder有泛型,但是OneListener并不需要關(guān)心。

OneViewHolder代碼如下:

/**
 * A ViewHolder that auto cast the data, from Object to the type you define
 * @param <D> the data type you define
 */
public abstract class OneViewHolder<D> extends RecyclerView.ViewHolder {

    public OneViewHolder(View itemView) {
        super(itemView);
    }

    public OneViewHolder(ViewGroup parent, int layoutRes) {
        super(LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false));
    }

    void bindView(int position, Object o){
        bindViewCasted(position, (D) o);
    }

    protected abstract void bindViewCasted(int position, D d);
}

OneViewHolder繼承自RecyclerView.ViewHolder,它將具體數(shù)據(jù)與視圖綁定。由于數(shù)據(jù)在OneAdapter中都是Object類型,為了調(diào)用者方便,這里利用泛型自動對數(shù)據(jù)進行了強制類型轉(zhuǎn)換。至于綁定視圖封裝了兩個方法:

  • OneViewHolder(ViewGroup parent, int layoutRes) 方法用于直接傳入ItemView的布局資源,用于大多數(shù)情況。
  • OneViewHolder(View itemView) 方法用于ItemView是自定義View的情況(此時要注意自定義View的LayoutParams)。

到這里,主要代碼就講完了。

示例

  1. 簡單列表


    SimpleList.png
public class SimpleListActivity extends AppCompatActivity {

    OneAdapter oneAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        oneAdapter = new OneAdapter(new OneListener() {
            @Override
            public boolean isMyItemViewType(int position, Object o) {
                return true;
            }

            @Override
            public OneViewHolder getMyViewHolder(ViewGroup parent) {
                return new OneViewHolder<String>(parent, R.layout.item_text){

                    @Override
                    protected void bindViewCasted(int position, String s) {
                        TextView text = itemView.findViewById(R.id.text);
                        text.setText(s);
                    }
                };
            }
        });

        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(oneAdapter);

        requestData();
    }

    private void requestData() {
        List<String> data = new ArrayList<>();
        for(int i = 'A'; i <= 'z'; i++) {
            data.add(" " + (char)i);
        }
        oneAdapter.setData(data);
        oneAdapter.notifyDataSetChanged();
    }
}
  1. 帶Header和Footer的列表
HeaderFooter.png
public class HeaderFooterActivity extends AppCompatActivity {
    RecyclerView recyclerView;
    OneAdapter oneAdapter;
    View footerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        oneAdapter = new OneAdapter(
                new OneListener() {
                    @Override
                    public boolean isMyItemViewType(int position, Object o) {
                        return position == 0;
                    }

                    @Override
                    public OneViewHolder getMyViewHolder(ViewGroup parent) {
                        return new OneViewHolder<Object>(parent, R.layout.item_text) {

                            @Override
                            protected void bindViewCasted(int position, Object o) {
                                TextView text = itemView.findViewById(R.id.text);
                                text.setText("This is header");
                            }
                        };
                    }
                },
                new OneListener() {
                    @Override
                    public boolean isMyItemViewType(int position, Object o) {
                        return o instanceof String;
                    }

                    @Override
                    public OneViewHolder getMyViewHolder(ViewGroup parent) {
                        return new OneViewHolder<String>(parent, android.R.layout.simple_list_item_1) {

                            @Override
                            protected void bindViewCasted(int position, String s) {
                                TextView text = itemView.findViewById(android.R.id.text1);
                                text.setText(s);
                            }
                        };
                    }
                },
                new OneListener() {
                    @Override
                    public boolean isMyItemViewType(int position, Object o) {
                        return position == oneAdapter.getItemCount() - 1;
                    }

                    @Override
                    public OneViewHolder getMyViewHolder(ViewGroup parent) {
                        return new OneViewHolder<Object>(footerView) {

                            @Override
                            protected void bindViewCasted(int position, Object o) {
                            }
                        };
                    }
                }
        );

        recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(oneAdapter);

        initFooterView();
        requestData();
    }

    private void initFooterView() {
        footerView = LayoutInflater.from(this).inflate(R.layout.item_text, recyclerView, false);
        ((TextView)footerView.findViewById(R.id.text)).setText("This is footer");
    }

    private void requestData() {
        List<Object> data = new ArrayList<>();
        data.add(null);
        for (int i = 'A'; i <= 'Z'; i++) {
            data.add(" " + (char) i);
        }
        data.add(null);
        oneAdapter.setData(data);
        oneAdapter.notifyDataSetChanged();
    }
}

擴展:Databinding支持

Databinding是Google推薦的做法,有了它就不需要寫 findViewById() 語句了,還能直接在Layout文件中綁定數(shù)據(jù)。 使用方法也很簡單,大家可以自己查一下相關(guān)教程。這里給OneAdapter添加Databinding支持。

對于OneAdapter來說,Databinding主要用于具體數(shù)據(jù)與視圖綁定,也就是OneViewHolder中所做的。OneViewHolder有兩個構(gòu)造方法,分別對應(yīng)自定義View和布局資源文件。對于自定義View來說,是否使用Databinding是調(diào)用者自己控制的。因此Databinding支持是針對使用布局資源文件的情況,這里封裝了一個包裝類OneViewHolderWrapper,用它替換OneViewHolder即可。

OneViewHolderWrapper代碼:

/**
 * A wrapper of OneViewHolder, supports data binding
 * @param <D> the type of the data
 * @param <B> the type of the ViewDataBinding
 */
public abstract class OneViewHolderWrapper<D,B extends ViewDataBinding>{

    private OneViewHolder<D> oneViewHolder;

    protected B binding;

    public OneViewHolderWrapper(ViewGroup parent, int layoutRes){
        binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutRes, parent, false);
        oneViewHolder = new OneViewHolder<D>(binding.getRoot()) {
            @Override
            protected void bindViewCasted(int position, D d) {
                OneViewHolderWrapper.this.bindViewCasted(position, d);
            }
        };
    }

    public OneViewHolder<D> getOneViewHolder() {
        return oneViewHolder;
    }

    protected abstract void bindViewCasted(int position, D d);
}

OneViewHolderWrapper中用D表示數(shù)據(jù)泛型,B表示ViewDataBinding泛型。binding對象用于綁定具體數(shù)據(jù)。

實際使用代碼:

public class DataBindingActivity extends AppCompatActivity {

    OneAdapter oneAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        oneAdapter = new OneAdapter(new OneListener() {
            @Override
            public boolean isMyItemViewType(int position, Object o) {
                return true;
            }

            @Override
            public OneViewHolder getMyViewHolder(ViewGroup parent) {
                return new OneViewHolderWrapper<Person, ItemPersonBinding>(parent, R.layout.item_person) {
                    @Override
                    protected void bindViewCasted(int position, Person person) {
                        binding.setPerson(person);
                        binding.executePendingBindings();
                    }
                }.getOneViewHolder();
            }
        });

        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(oneAdapter);

        requestData();
    }

    private void requestData() {
        List<Object> data = new ArrayList<>();
        for(int i = 0; i <= 10; i++) {
            data.add(new Person("Bill", 22));
            data.add(new Person("Chris", 10));
            data.add(new Person("David", 36));
        }
        oneAdapter.setData(data);
        oneAdapter.notifyDataSetChanged();
    }
}

需要OneViewHolder實例時,先創(chuàng)建包裝類OneViewHolderWrapper實例,然后調(diào)用 getOneViewHolder() 方法從包裝類中取得OneViewHolder實例。這樣原有的OneAdapter和OneListener都直接兼容。

Databinding.png

擴展:下拉刷新和加載更多

用SwipeRefreshLayout和FooterView實現(xiàn)了簡單的下拉刷新和加載更多功能,這是對OneAdapter的簡單擴展。沒有加入EmptyView,因為EmptyView可以完全在外部控制。

public class RecyclerLayout extends SwipeRefreshLayout implements OnRefreshListener, LoadingLayout.OnLoadingListener {

    private RecyclerView recyclerView;
    private LoadingLayout loadingLayout;

    private OneAdapter oneAdapter;
    private GridLayoutManager gridLayoutManager;

    private OnRefreshListener onRefreshListener;
    private LoadingLayout.OnLoadingListener onLoadingListener;

    public RecyclerLayout(Context context) {
        this(context, null);
    }

    public RecyclerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnRefreshListener(this);

        loadingLayout = new LoadingLayout(context);

        gridLayoutManager = new GridLayoutManager(context, 1);
        recyclerView = new RecyclerView(context);
        recyclerView.setLayoutManager(gridLayoutManager);
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {

            int lastVisibleItemPosition;

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                lastVisibleItemPosition = gridLayoutManager.findLastVisibleItemPosition();
            }

            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                if (newState == RecyclerView.SCROLL_STATE_IDLE && lastVisibleItemPosition == oneAdapter.getItemCount() - 1 - 1) {
                    // load more
                    onLoading();
                }
            }
        });

        addView(recyclerView);
    }

    public void init(final OneAdapter oneAdapter, OnRefreshListener onRefreshListener, LoadingLayout.OnLoadingListener onLoadingListener){
        this.recyclerView.setAdapter(oneAdapter);
        this.oneAdapter = oneAdapter;
        this.oneAdapter.getListeners().add(0, new OneListener() {
            @Override
            public boolean isMyItemViewType(int position, Object o) {
                return position == oneAdapter.getItemCount() - 1;
            }

            @Override
            public OneViewHolder getMyViewHolder(ViewGroup parent) {
                return new OneViewHolder(loadingLayout) {
                    @Override
                    protected void bindViewCasted(int position, Object o) {
                        //ignore
                    }
                };
            }
        });

        if(onRefreshListener == null){
            setEnabled(false);
        }
        this.onRefreshListener = onRefreshListener;
        this.onLoadingListener = onLoadingListener;
    }

    @Override
    public void onRefresh() {
        if(onRefreshListener != null){
            onRefreshListener.onRefresh();
        }
    }

    @Override
    public void onLoading() {
        if(onLoadingListener != null && !isRefreshing() && !isLoading() && !isNoMore()){
            onLoadingListener.onLoading();
            setLoading(true, isNoMore());
        }
    }

    public void setData(List<?> data, boolean hasMore){
        data.add(null);
        oneAdapter.setData(data);
        oneAdapter.notifyDataSetChanged();

        setRefreshing(false);
        setLoading(false, !hasMore);
    }

    public void addData(List<?> data, boolean hasMore){
        List<Object> cur = oneAdapter.getData();
        if(!cur.isEmpty()){
            cur.remove(cur.size() - 1);
        }
        data.add(null);
        oneAdapter.addData(data);
        oneAdapter.notifyDataSetChanged();

        setLoading(false, !hasMore);
    }

    private boolean isNoMore(){
        return loadingLayout.isNoMore();
    }

    private boolean isLoading(){
        return loadingLayout.isLoading();
    }

    private void setLoading(boolean loading, boolean isNoMore){
        loadingLayout.setLoading(loading, isNoMore);
    }
}

實際使用如下:

public class RefreshActivity extends AppCompatActivity {

    RecyclerLayout recyclerLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        recyclerLayout = new RecyclerLayout(this);
        setContentView(recyclerLayout);

        OneAdapter oneAdapter = new OneAdapter(
                new OneListener() {

                    @Override
                    public boolean isMyItemViewType(int position, Object o) {
                        return true;
                    }

                    @Override
                    public OneViewHolder getMyViewHolder(ViewGroup parent) {

                        return new OneViewHolder<String>(parent, R.layout.item_text) {
                            @Override
                            protected void bindViewCasted(int position, String s) {
                                TextView text = itemView.findViewById(R.id.text);
                                text.setText(s);
                            }
                        };
                    }
                }
        );

        recyclerLayout.init(oneAdapter,
                new SwipeRefreshLayout.OnRefreshListener() {
                    @Override
                    public void onRefresh() {
                        requestData();
                    }
                },
                new LoadingLayout.OnLoadingListener() {
                    @Override
                    public void onLoading() {
                        requestMoreData();
                    }
                }
        );


        recyclerLayout.setRefreshing(true);
        requestData();
    }

    int page;

    private void requestData() {
        getWindow().getDecorView().postDelayed(new Runnable() {
            @Override
            public void run() {

                List<Object> data = new ArrayList<>();
                for (int i = 'A'; i <= 'Z'; i++) {
                    String s = (char) i + " " + System.nanoTime();
                    data.add(s);
                }

                page = 0;
                recyclerLayout.setData(data, page++ < 2);

            }
        }, 1000);
    }

    private void requestMoreData() {
        getWindow().getDecorView().postDelayed(new Runnable() {
            @Override
            public void run() {

                List<Object> data = new ArrayList<>();
                for (int i = 'A'; i <= 'Z'; i++) {
                    String s = (char) i + " " + System.nanoTime();
                    data.add(s);
                }

                recyclerLayout.addData(data, page++ < 2);

            }
        }, 1000);
    }

}

代碼結(jié)構(gòu)

oneadapter.png
  • 實現(xiàn)普通或多種類型的RecyclerView,使用base包中的類即可;
  • 如果需要Databinding支持,加入databinding包中的類;
  • 如果需要下拉刷新和加載更多,可以參考refresh包中的實現(xiàn)。

Github地址:https://github.com/rome753/OneAdapter
完整代碼和Demo示例都在這里,歡迎Fork和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ù)。

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