使用RecyclerView創(chuàng)建性能更好的列表界面

Android5.0發(fā)布后為我們帶來了新的控件RecyclerView。RecyclerView被稱為L(zhǎng)istView和GridView的繼任者,它使用一種統(tǒng)一的方式整合了這兩種視圖的使用并提供更多選擇。通過使用RecyclerView結(jié)合不同的LayoutManager我們可以一次性實(shí)現(xiàn)線性,網(wǎng)格瀑布流三種布局,如果需要,線性列表不再一定是垂直的,還可以橫向滾動(dòng)。此外,RecyclerView還封裝了更好的ViewHolder和Adapter類,將原來使用ListView時(shí)必須由開發(fā)者來實(shí)現(xiàn)的復(fù)用優(yōu)化工作封裝好。開發(fā)者寫更少的代碼就能避免性能不佳的問題。這篇短文通過對(duì)比ListView和RecyclerView的不同使用方式,來說明為什么我們應(yīng)該優(yōu)先使用RecyclerView。
Android App中開發(fā)列表界面可以做的很簡(jiǎn)單,每個(gè)列表項(xiàng)僅顯示一行文字;也可以做的很復(fù)雜,每個(gè)列表項(xiàng)包含標(biāo)題,副標(biāo)題,按鈕,復(fù)選框,還能響應(yīng)點(diǎn)擊事件等等。這些視圖組件在加載時(shí)需要父視圖通過findViewById()一個(gè)一個(gè)創(chuàng)建。這個(gè)函數(shù)通過遍歷整個(gè)布局資源來查找目標(biāo)視圖組件,如果用戶每滑動(dòng)一次列表就遍歷一遍甚至多遍,就會(huì)影響App的性能,因此兩種列表布局都使用ViewHolder來解決這個(gè)問題。此外,因?yàn)榭梢葬槍?duì)不同的數(shù)據(jù)模型定制列表項(xiàng),所以ListView或者RecyclerView需要有輔助類來負(fù)責(zé)將數(shù)據(jù)模型適配到視圖組件中,這就需要借助“適配器模式”來實(shí)現(xiàn)。ListView使用的是ArrayAdapter,而RecyclerView使用的則是Adapter。下面讓我們通過對(duì)比,來看看RecyclerView的實(shí)現(xiàn)方式為什么比ListView的實(shí)現(xiàn)方式要好。

一. 使用ListView創(chuàng)建列表界面

1. ListView,ArrayAdapter和ViewHolder

使用ListView來創(chuàng)建列表項(xiàng)主要依賴ArrayAdapter和ViewHolder這兩個(gè)輔助類。我們創(chuàng)建一個(gè)ArrayAdapter的子類,然后在子類中定義一個(gè)ViewHolder的輔助類,這個(gè)輔助類負(fù)責(zé)托管視圖組件。然后最重要的邏輯通過重載ArrayAdapter的getView()方法來實(shí)現(xiàn)。下面通過一個(gè)具體的代碼示例來看看。

2. 代碼示例

假設(shè)我們要實(shí)現(xiàn)一個(gè)功能:通過網(wǎng)絡(luò)請(qǐng)求獲取數(shù)據(jù)并更新列表。那么我們首先需要在Activity或者Fragment中創(chuàng)建好ArrayAdapter子類,傳入列表布局資源和數(shù)據(jù)模型;初始化ListView,并在ListView上調(diào)用setAdapter()設(shè)置適配器,別忘了,一定要記得在數(shù)據(jù)模型發(fā)生更新時(shí)調(diào)用ArrayAdapter的notifyDataSetChanged()函數(shù)通知列表更新。

public class NewTaskFragment extends Fragment {
    private static String TAG = NewTaskFragment.class.getSimpleName();
    private List<CheckItem> mNewTaskList = new ArrayList<>();
    private TaskFragmentAdapter mAdapter;
    ...
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        Logger.t(TAG).d(TAG, "onCreateView");
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_new_task, container, false);
        ...
        mAdapter = new TaskFragmentAdapter(getContext(), R.layout.task_item, mNewTaskList, true);
        ListView newTaskView = (ListView) view.findViewById(R.id.new_task_list);
        newTaskView.setAdapter(mAdapter);
        return view;
    }

    private void refreshNewTask() {
        ...
        mNewTaskList.add(taskItem);
        mAdapter.notifyDataSetChanged();
    }

}

最重要的工作都放到ArrayAdapter的子類中,其中要特別注意的就是判斷視圖是不是第一次創(chuàng)建。如果是第一次創(chuàng)建,那么就需要通過ViewHolder初始化視圖組件,并setTag()緩存到View中去。如果第一次(比如用戶滾動(dòng)列表刷新界面),就通過getTag()獲取已經(jīng)創(chuàng)建好的視圖組件,然后用對(duì)應(yīng)數(shù)據(jù)模型進(jìn)行更新,這幾乎就是重載ArrayAdapter的getView()方法的標(biāo)準(zhǔn)套路。

public class TaskFragmentAdapter extends ArrayAdapter<CheckItem> {
    private int mResID;
    ...

    public TaskFragmentAdapter(Context context, int resource, List<CheckItem> objects, boolean newTask) {
        super(context, resource, objects);
        mResID = resource;
        ...
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        final CheckItem item = getItem(position);
        View view;
        ViewHolder viewHolder;
        if (convertView == null) {
            view = LayoutInflater.from(getContext()).inflate(mResID, null);
            viewHolder = new ViewHolder();
            viewHolder.image = (ImageView) view.findViewById(R.id.type_image);
            viewHolder.addrView = (TextView) view.findViewById(R.id.task_name);
            viewHolder.macView = (TextView) view.findViewById(R.id.mac);
            viewHolder.checkButton = (Button) view.findViewById(R.id.check);
            view.setTag(viewHolder);
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.image.setImageResource(item.getTypeIndicatorID());
        viewHolder.addrView.setText(item.getAddress());
        viewHolder.macView.setText(item.getMAC());
        viewHolder.checkButton.setOnClickListener(new OnClickListener() {
            ...
        });
        return view;
    }

    class ViewHolder {
        ImageView image;
        TextView addrView;
        TextView macView;
        Button checkButton;
    }
}

二. 使用RecyclerView創(chuàng)建列表界面

在使用了RecyclerView之后這些工作都不用開發(fā)者自己寫了,因?yàn)镽ecyclerView都會(huì)給你做好。使用RecyclerView最好的一點(diǎn)是,它減少了開發(fā)者不必要的工作量,使得代碼看上去更規(guī)范,工整。它將這些優(yōu)化工作封裝到了庫(kù)中,因此開發(fā)者只需要在Adapter中重載幾個(gè)函數(shù),就能做到高性能實(shí)現(xiàn)。這也從更大程度上防止初學(xué)者寫出不太好的代碼。

1. RecyclerView,Adapter和ViewHolder

RecyclerView結(jié)合Adapter和ViewHolder,圍繞“適配器模式”提供了一套滿足“單一職責(zé)原則”的解決方案。

“一個(gè)類應(yīng)該只有一個(gè)發(fā)生變化的原因”

其中RecyclerView作為ViewGroup的子類,負(fù)責(zé)展示列表項(xiàng),每個(gè)列表項(xiàng)都是View的子對(duì)象。RecyclerView并不是有多少項(xiàng)就創(chuàng)建多少項(xiàng),這樣很容易搞垮應(yīng)用。當(dāng)用戶滑動(dòng)屏幕時(shí),滑出視圖的列表項(xiàng)會(huì)被回收用于顯示新的列表項(xiàng),這就是“RecyclerView”這個(gè)名字的由來。ViewHolder不變,仍然負(fù)責(zé)托管和容納視圖組件,這個(gè)沒什么好說的。而Adapter的使用就規(guī)范多了,只需要重載三個(gè)函數(shù)即可:

  1. onCreateViewHolder(ViewGroup parent, int viewType)
  2. onBindViewHolder(VH holder, int position)
  3. getItemCount()

其中onCreateViewHolder()用于創(chuàng)建ViewHolder對(duì)象,onBindViewHolder()用于將數(shù)據(jù)模型綁定到ViewHolder中的視圖組件上,getItemCount()返回?cái)?shù)據(jù)模型的數(shù)量。當(dāng)RecyclerView需要顯示視圖時(shí),就會(huì)通過它的Adapter來實(shí)現(xiàn),大致流程如下:

  1. RecyclerView調(diào)用Adapter的getItemCount()方法詢問數(shù)組列表中有多少數(shù)據(jù);
  2. RecyclerView調(diào)用Adapter的createViewHolder()創(chuàng)建ViewHolder及其要顯示的視圖;
  3. RecyclerView調(diào)用Adapter的onBindViewHolder()方法,傳入ViewHolder對(duì)象及位置,Adapter找到目標(biāo)位置對(duì)應(yīng)的數(shù)據(jù),用它綁定到ViewHolder對(duì)象容納的視圖上。

其中createViewHolder()調(diào)用并不頻繁。一旦創(chuàng)建了足夠的ViewHolder,RecyclerView就不會(huì)再調(diào)用createViewHolder(),而是回收利用舊的ViewHolder來節(jié)約內(nèi)存開銷。

2. 代碼示例

比如我們要實(shí)現(xiàn)如下所示的列表界面:

示例列表

只需要在Fragment或Activity中編寫初始化代碼,并定義好ViewHolder和Adapter即可,注意RecyclerView多出來一步設(shè)置LayoutManager。

public class CrimeListFragment extends Fragment {
    RecyclerView mCrimeRecyclerView;
    CrimeAdapter mCrimeAdapter;

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_crime_list, container, false);
        mCrimeRecyclerView = (RecyclerView) view.findViewById(R.id.crime_recycler_view);
        mCrimeRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        updateUI();
        return view;
    }

    private class CrimeHolder extends RecyclerView.ViewHolder ... {
        private TextView mTitleTextView;
        private TextView mDateTextView;
        private CheckBox mSolvedCheckBox;

        public CrimeHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(this);
            mTitleTextView = (TextView) itemView.findViewById(R.id.list_item_crime_title_text_view);
            mDateTextView = (TextView) itemView.findViewById(R.id.list_item_crime_date_text_view);
            mSolvedCheckBox = (CheckBox) itemView.findViewById(R.id.list_item_crime_solved_check_box);
        }

        public void bindCrime(Crime crime) {
            mTitleTextView.setText(crime.getTitle());
            mDateTextView.setText(crime.getDate().toString());
            mSolvedCheckBox.setChecked(crime.isSolved());
        }

        ...
    }

    private class CrimeAdapter extends RecyclerView.Adapter<CrimeHolder> {
        private List<Crime> mCrimes;

        public CrimeAdapter(List<Crime> crimes) {
            mCrimes = crimes;
        }

        @Override
        public CrimeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater inflater = LayoutInflater.from(getActivity());
            View view = inflater.inflate(R.layout.list_item_crime, parent, false);
            return new CrimeHolder(view);
        }

        @Override
        public void onBindViewHolder(CrimeHolder holder, int position) {
            Crime crime = mCrimes.get(position);
            holder.bindCrime(crime);
        }

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

    private void updateUI() {
        mCrimeAdapter = new CrimeAdapter(CrimeLab.get(getActivity()).getCrimes());
        mCrimeRecyclerView.setAdapter(mCrimeAdapter);
    }
}

使用RecyclerView寫出來的代碼要比ListView更容易理解,所以在項(xiàng)目開發(fā)中強(qiáng)烈建議使用這個(gè)新的組件。

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

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

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