Android:打造“萬能”Adapter與ViewHolder

iamxiarui_發表於2018-07-02

##寫在前面

最近一直忙著各種結課大作業,重新看起Android還有種親切感。前段時間寫專案的時候,學習了一個萬能Adapter與ViewHolder的寫法。說是“萬能”其實就是在各種情況下都能通用。

我們知道,在寫專案的時候,專案中肯定有很多的ListView或者RecyclerView,這個時候我們就要寫大量的Adapter與ViewHolder。儘管重複寫的難度並不大,但是這會讓專案看起來十分冗餘,因為存在大量的重複程式碼。

所以能不能有一個通用的ViewHolder與Adapter,讓專案中只存在一個ViewHolder與Adapter呢?

當然可以,現在就通過一個小Demo將我學習的知識分享給大家。下面是本文的目錄:

  • 專案介紹
  • 傳統寫法分析
  • 簡單認識SparseArray
  • 萬能ViewHolder
  • 萬能Adapter
  • 結語
  • 專案原始碼

##專案介紹

先來看這個Demo,很簡單,我就不多說了。

專案圖

這是專案結構,為了方便後期對比,我將三種Adapter分離開了:

專案結構

  • MainActivity:模擬新聞頁面
  • NewsBean:封裝了新聞的Bean
  • CommonViewHolder:通用ViewHolder
  • CommonAdapter:通用Adapter
  • TraditionAdapterWithTraditionHolder:基於傳統Holder的傳統Adapter
  • TraditionAdapterWithCommonHolder:基於通用ViewHolder的傳統Adapter
  • CommonAdapterWithCommoeHolder:基於通用ViewHolder的通用Adapter

##傳統寫法分析

至於頁面佈局、模擬載入資料在這裡我就不提了,十分簡單。現在主要看一下傳統的Adapter的寫法。

/**
 * 基於傳統Holder的傳統Adapter
 */
public class TraditionAdapterWithTraditionHolder extends BaseAdapter {
    private Context context;
    private List<NewsBean> list;

    public TraditionAdapterWithTraditionHolder(Context context, List<NewsBean> list) {
        this.context = context;
        this.list = list;
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder ;
        if (convertView == null) {
            convertView = View.inflate(context, R.layout.item_list, null);
            viewHolder = new ViewHolder();

            viewHolder.titleText = (TextView) convertView.findViewById(R.id.tv_title);
            viewHolder.descText = (TextView) convertView.findViewById(R.id.tv_desc);
            viewHolder.timeText = (TextView) convertView.findViewById(R.id.tv_time);
            viewHolder.phoneText = (TextView) convertView.findViewById(R.id.tv_phone);

            convertView.setTag(viewHolder);
            
        }else{
            viewHolder = (ViewHolder) convertView.getTag();
        }

        NewsBean bean = list.get(position);

        viewHolder.titleText.setText(bean.getTitle());
        viewHolder.descText.setText(bean.getDesc());
        viewHolder.timeText.setText(bean.getTime());
        viewHolder.phoneText.setText(bean.getPhone());

        return convertView;
    }

    private class ViewHolder {
        TextView titleText;
        TextView descText;
        TextView timeText;
        TextView phoneText;
    }
}
複製程式碼

由於程式碼也比較簡單,基本都是 套路 程式碼,大家都會寫,都能看懂,所以我就不加以詳細註釋了。

全都是套路

我們知道,如果需要一個通用的Adapter,肯定要對之前的程式碼進行封裝。所以現在主要來分析一下這個傳統寫法,看到底哪個地方可以進行封裝。

依次來看,首先是建構函式。只要稍微有點經驗的開發者都知道,一般來說,這裡面傳遞的引數幾乎都是一個 ContextList ,而List中通常都裝了一個具體內容的 Bean

public TraditionAdapterWithTraditionHolder(Context context, List<NewsBean> list) {
		this.context = context;
		this.list = list;
}
複製程式碼

所以設想,既然這個Bean每次都需要,那我們是否能否將這個Bean直接給自定義的Adapter呢?比如這樣:

public MyAdapter<T>(Context context, List<T> list) {
		this.context = context;
		this.list = list;
}
複製程式碼

先把這個問題拋向天空,來看固定的三個方法,這也沒啥好說的,依舊是套路,所以可以封裝成固定方法,內部實現它,不需暴露出來再複寫:

@Override
public int getCount() {
	return list.size();
}

@Override
public Object getItem(int position) {
	return list.get(position);
}

@Override
public long getItemId(int position) {
	return position;
}
複製程式碼

然後在重頭戲 getView 方法中需要一個ViewHolder,來複用已有的View。然後 new 出List中的 Bean ,賦值後顯示在View上。這就是基本的套路。

private class ViewHolder {
    TextView titleText;
    TextView descText;
    TextView timeText;
    TextView phoneText;
}

Override
public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder viewHolder ;
    if (convertView == null) {
        convertView = View.inflate(context, R.layout.item_list, null);
        viewHolder = new ViewHolder();

        viewHolder.titleText = (TextView) convertView.findViewById(R.id.tv_title);
        viewHolder.descText = (TextView) convertView.findViewById(R.id.tv_desc);
        viewHolder.timeText = (TextView) convertView.findViewById(R.id.tv_time);
        viewHolder.phoneText = (TextView) convertView.findViewById(R.id.tv_phone);

        convertView.setTag(viewHolder);

    }else{
        viewHolder = (ViewHolder) convertView.getTag();
    }

    NewsBean bean = list.get(position);

    viewHolder.titleText.setText(bean.getTitle());
    viewHolder.descText.setText(bean.getDesc());
    viewHolder.timeText.setText(bean.getTime());
    viewHolder.phoneText.setText(bean.getPhone());

    return convertView;
}
複製程式碼

而這個ViewHolder套路就更深了,先定義一個ViewHolder類,類中是佈局中所需的控制元件,然後在getView方法中new一個ViewHolder出來,通過這個ViewHolder找到對應的控制元件,找到後需要設定個Tag,方便之後複用。最後就是通過ViewHolder設定控制元件的內容了。

既然熟悉了過程,那封裝起來就簡單了許多。首先肯定需要封裝ViewHolder類,不然怎麼算的上通用,但是每一個ListView中item佈局可能不一樣,肯定不能將控制元件寫死,那麼如何定義控制元件呢?當控制元件定義好後,又如何找到這些控制元件呢?控制元件找到後又如何設定控制元件內容呢?

仍然將這些問題拋向天空,接下來再考慮convertView的複用問題,固定寫法,當然也可以封裝。

所以目前來看,如果想要一個Adapter與ViewHolder可以通用,那麼 至少 必須做如下工作:

  • 將List的泛型引數轉移到Adapter中
  • 封裝 getCount、getItem、getItemId方法
  • 封裝ViewHolder,並解決不同佈局控制元件不統一問題
  • 通用ViewHolder需要找到相應的控制元件
  • 通用ViewHolder需要提供方法來設定相應控制元件的內容

##簡單認識SparseArray

在寫萬能ViewHolder之前,先來了解一個新的API。我們知道,在Java中一般會用HashMap以鍵值對的形式來儲存一些資料。但是Android給我們提供了一種工具類 SparseArray ,它是Android框架獨有的類,在標準的JDK中不存在這個類。

為什麼需要用SparseArray代替HashMap呢?

SparseArray要比 HashMap 節省記憶體,某些情況下比HashMap效能更好

那為什麼SparseArray效能更好呢?按照官方的解釋,原因有以下幾點:

  • SparseArray不需要對key和value進行自動裝箱
  • 結構比HashMap簡單
  • SparseArray內部主要使用兩個一維陣列來儲存資料,一個用來存key,一個用來存value
  • 不需要額外的資料結構(主要是針對HashMap中的HashMapEntry 而言的)

從原始碼的建構函式來看,與List一樣,可以通過new的形式來建立一個SparseArray,與Map一樣,可以通過 put(int key, E value) 的形式來新增鍵值對。也可以通過 get(int key) 的方式來獲取值。

好了,就介紹這麼多,關於具體的用法,文末附有參考資料連結,如有需要可以自行檢視。

##萬能ViewHolder

現在就來打造萬能ViewHolder,打造之前再次明確我們需要做的事情:

  • 提供方法返回ViewHolder
  • 提供方法獲取控制元件
  • 提供方法對控制元件進行設定
  • 提供方法返回複用的View,也就是convertView

先來看如何解決不同佈局有不同控制元件的問題。由於每個控制元件都有自己固定的ID和控制元件型別,那麼我們可以通過鍵值對的形式來儲存這些控制元件。在之前可以看到SparseArray能夠提高效能,所以就用SparseArray來儲存控制元件。

這樣可以先寫出建構函式,在建構函式中,初始化SparseArray,並設定一些內容。

/**
 * 通用ViewHolder
 */
public class CommonViewHolder {

    //所有控制元件的集合
    private SparseArray<View> mViews;
	//記錄位置 可能會用到
    private int mPosition;
	//複用的View
    private View mConvertView;
複製程式碼

    /**
     * 建構函式
     *
     * @param context  上下文物件
     * @param parent   父類容器
     * @param layoutId 佈局的ID
     * @param position item的位置
     */
    public CommonViewHolder(Context context, ViewGroup parent, int layoutId, int position) {
        this.mPosition = position;
        this.mViews = new SparseArray<>();
		//構造方法中就指定佈局
        mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);
		//設定Tag
        mConvertView.setTag(this);
    }
}
複製程式碼

接下來我們就需要得到一個ViewHolder,這個比較簡單,大家都能看懂,就是對Adapter中的getView方法進行一定的封裝:

/**
 * 得到一個ViewHolder
 *
 * @param context     上下文物件
 * @param convertView 複用的View
 * @param parent      父類容器
 * @param layoutId    佈局的ID
 * @param position    item的位置
 * @return
 */
public static CommonViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {
    //如果為空  直接新建一個ViewHolder
    if (convertView == null) {
        return new CommonViewHolder(context, parent, layoutId, position);
    } else {
        //否則返回一個已經存在的ViewHolder
        CommonViewHolder viewHolder = (CommonViewHolder) convertView.getTag();
        //記得更新條目位置
        viewHolder.mPosition = position;
        return viewHolder;
    }
}
複製程式碼

再接下來就是一個重難點,如何得到佈局中的控制元件?因為我們肯定知道控制元件的ID,那麼可以通過控制元件的ID來從SparseArray得到具體的控制元件型別。而Android中所有的控制元件都是繼承自 View ,所以可以如下這樣寫:

/**
 * 通過ViewId獲取控制元件
 *
 * @param viewId View的Id
 * @param <T>    View的子類
 * @return 返回View
 */
public <T extends View> T getView(int viewId) {
    View view = mViews.get(viewId);
    if (view == null) {
        view = mConvertView.findViewById(viewId);
        mViews.put(viewId, view);
    }
    return (T) view;
}
複製程式碼

通過上述方法,就能得到對應的控制元件型別。既然得到了,那麼設定控制元件內容就比較簡單了,在本例中都是TextView,所以我封裝了下面的方法:

/**
 * 為文字設定text
 *
 * @param viewId view的Id
 * @param text   文字
 * @return 返回ViewHolder
 */
public CommonViewHolder setText(int viewId, String text) {
    TextView tv = getView(viewId);
    tv.setText(text);
    return this;
}
複製程式碼

最後提供一個方法返回複用的convertView,這也比較簡單。

 /**
 * @return 返回複用的View
 */
public View getConvertView() {
    return mConvertView;
}
複製程式碼

好了,再來看全部的程式碼,是不是清晰了很多:

/**
 * 通用ViewHolder
 */
public class CommonViewHolder {

    //所有控制元件的集合
    private SparseArray<View> mViews;
	//記錄位置 可能會用到
    private int mPosition;
	//複用的View
    private View mConvertView;
複製程式碼

    /**
     * 建構函式
     *
     * @param context  上下文物件
     * @param parent   父類容器
     * @param layoutId 佈局的ID
     * @param position item的位置
     */
    public CommonViewHolder(Context context, ViewGroup parent, int layoutId, int position) {
        this.mPosition = position;
        this.mViews = new SparseArray<>();
        mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        mConvertView.setTag(this);
    }

    /**
     * 得到一個ViewHolder
     *
     * @param context     上下文物件
     * @param convertView 複用的View
     * @param parent      父類容器
     * @param layoutId    佈局的ID
     * @param position    item的位置
     * @return
     */
    public static CommonViewHolder get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {
        //如果為空  直接新建一個ViewHolder
        if (convertView == null) {
            return new CommonViewHolder(context, parent, layoutId, position);
        } else {
            //否則返回一個已經存在的ViewHolder
            CommonViewHolder viewHolder = (CommonViewHolder) convertView.getTag();
            //記得更新條目位置
            viewHolder.mPosition = position;
            return viewHolder;
        }
    }

    /**
     * @return 返回複用的View
     */
    public View getConvertView() {
        return mConvertView;
    }

    /**
     * 通過ViewId獲取控制元件
     *
     * @param viewId View的Id
     * @param <T>    View的子類
     * @return 返回View
     */
    public <T extends View> T getView(int viewId) {
        View view = mViews.get(viewId);
        if (view == null) {
            view = mConvertView.findViewById(viewId);
            mViews.put(viewId, view);
        }
        return (T) view;
    }

    /**
     * 為文字設定text
     *
     * @param viewId view的Id
     * @param text   文字
     * @return 返回ViewHolder
     */
    public CommonViewHolder setText(int viewId, String text) {
        TextView tv = getView(viewId);
        tv.setText(text);
        return this;
    }
}
複製程式碼

接下來我們就重寫一個基於萬能ViewHolder的Adapter,其他方法都不變,主要是getView方法。

@Override
public View getView(int position, View convertView, ViewGroup parent) {

	//得到一個ViewHolder
    CommonViewHolder viewHolder = CommonViewHolder.get(context, convertView, parent, R.layout.item_list, position);

    NewsBean bean = list.get(position);

	//直接設定控制元件內容,鏈式呼叫
    viewHolder.setText(R.id.tv_title, bean.getTitle())
            .setText(R.id.tv_desc, bean.getDesc())
            .setText(R.id.tv_time, bean.getTime())
            .setText(R.id.tv_phone, bean.getPhone());

	//返回複用的View
    return viewHolder.getConvertView();
}
複製程式碼

現在來與之前的方法對比,是不是簡單了很多,只需三步:

  • 得到一個ViewHolder
  • 通過這個ViewHolder直接設定控制元件內容
  • 返回複用的View

看到這裡大家肯定有個疑問,在上面ViewHolder中只提供了TextView設定文字的方法,那如果控制元件不是TextView呢?沒關係,繼續在萬能ViewHolder中封裝就好了:

/**
 * 設定ImageView
 *
 * @param viewId view的Id
 * @param resId  資源Id
 * @return
 */
public CommonViewHolder setImageResource(int viewId, int resId) {
    ImageView iv = getView(viewId);
    iv.setImageResource(resId);
    return this;
}

/**
 * 還可以新增更多的方法
 */
複製程式碼

至此,我們就搞定了一個通用的“萬能”ViewHolder。

##萬能Adapter

有了萬能ViewHolder,我們就可以來打造萬能Adapter了,在文章開頭已經分析過,需要做的事情有一下幾點:

  • 將Bean物件直接設定成Adapter的泛型
  • 封裝三個固定方法
  • 封裝getView方法
  • 提供方法設定控制元件內容

先直接上程式碼,其實比較簡單,大家應該能看懂:

/**
 * 通用Adapter抽象類
 */
public abstract class CommonAdapter<T> extends BaseAdapter {

    protected Context context;
    protected List<T> list;
    private int layoutId;

    public CommonAdapter(Context context, List<T> list, int layoutId) {
        this.context = context;
        this.list = list;
        this.layoutId = layoutId;
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public T getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    /**
     * 封裝getView方法
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //得到一個ViewHolder
        CommonViewHolder viewHolder = CommonViewHolder.get(context, convertView, parent, layoutId, position);

        //設定控制元件內容
        setViewContent(viewHolder, (T) getItem(position));

        //返回複用的View
        return viewHolder.getConvertView();
    }

    /**
     * 提供抽象方法,來設定控制元件內容
     *
     * @param viewHolder 一個ViewHolder
     * @param t          一個資料集
     */
    public abstract void setViewContent(CommonViewHolder viewHolder, T t);
}
複製程式碼

這裡可以看到我們先自定義一個Adapter繼承BaseAdapter,並將Bean換成Adapter的泛型T了,然後封裝了四個方法。又由於各個控制元件不一樣,所以提供抽象方法來設定控制元件內容,我們只要複寫就行了。

此時我們再來看基於萬能ViewHolder的萬能Adapter應該怎樣寫:

/**
 * 繼承通用Adapter且使用通用Holder的介面卡
 */
public class CommonAdapterWithCommonHolder extends CommonAdapter<NewsBean> {

    public CommonAdapterWithCommonHolder(Context context, List<NewsBean> list) {
        super(context, list,R.layout.item_list);
    }

    /**
     * 複寫抽象方法
     * @param viewHolder 一個ViewHolder
     * @param bean Bean物件
     */
    @Override
    public void setViewContent(CommonViewHolder viewHolder, NewsBean bean) {

        //直接設定內容 鏈式呼叫
        viewHolder.setText(R.id.tv_title, bean.getTitle())
                .setText(R.id.tv_desc, bean.getDesc())
                .setText(R.id.tv_time, bean.getTime())
                .setText(R.id.tv_phone, bean.getPhone());
    }
}
複製程式碼

看到這裡,是不是有點神奇,對比之前的Adapter,這裡只要幾行程式碼就OK了。

##結語

由於本文說明的不是一種固定的知識,而是一種設計的思想,所以理解起來比較晦澀難懂。我自己在學這個的時候,也是消化了很久,現在回頭看看真的是很巧妙。

不過值得注意的是,這裡說的“萬能”其實就是一個俗稱,代表一種通用的Adapter,能避免專案中的大量的重複程式碼,提高程式碼質量。而這種通用,不一定就是文中的這樣的格式,這裡只是提供一個設計思想與大致流程,大家可以自己寫一個通用的、更加強大的Adapter。

最後由於我水平有限與篇幅限制等原因,在寫文章的過程中,有很多地方寫的不夠詳細或者有明顯的疏漏與錯誤,歡迎大家交流與指正。

##參考資料

Android應用效能優化之使用SparseArray替代HashMap

SparseArray替代HashMap來提高效能

如何打造萬能介面卡

##專案原始碼 CommonAdapter-GitHub-IamXiaRui


個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=727

相關文章