2018年11月7日 星期三

隨手札記:(Android) RecyclerView、Adapter與他的快樂夥伴

因緣際會之下我開始了App工程師的開發生活(咦
廢話不多說,直接進入重點。

所以我說,那個RecyclerView到底是瞎....毀?




一言以蔽之:RecyclerView是Android中由Google官方提供,提供重複使用的項目、並提升效能的清單檢視元件

他和他快樂的夥伴——Adaper、ViewHolder、還有LayoutManager、跟data時常結伴出現。

用圖解的方式來解釋他們之間的關係,大概是這樣:

Android RecyclerView圖解

什麼是RecyclerView

簡單來說,他是一個清單View元件。
如果我們想要置入的清單內容只有不到10項,那我們可能可以在前端刻死。
可是,如果清單內容超過100項、甚至1000項呢?
這時候使用RecyclerView就有顯著的效果了:

RecyclerView--相較於一般清單View元件--並不會創建與項目數量相同的View。相反地,它只會創建合適數量的View,並將這些View重複使用。

可以想像成:若說一般元件是一般餐廳,每一道料理都得用一個盤子裝;而RecyclerView則是爭X迴轉壽司——當這一盤壽司出現在你面前之後再度回到後台,料理人員可以把上面的壽司更換、然後再把這個盤子送回來給你看(欸)

RecyclerView可以大幅節省App記憶體資源使用量,而這件事是因為它不像其他View一樣,需要java腳本不停呼叫FindViewByID()
*FindViewByID()雖然是串聯前後端常見的手段,然而它相當地吃效能。可以少用則儘量少用。

所以我要怎麼使用RecyclerView

這個可以分為兩個要點:如何指派資料給RecyclerView、以及如何指派/更改RecyclerView的項目顯示方式。

然而這兩個要點都跟一個元件有關:萬惡的Adapter元件。

什麼是Adapter元件

Adapter、或稱為「適配器」:你可以把他想像為前端介面跟後端資料中間的「仲介」——當你需要指派(大量的)資料給介面顯示時,一般的作法就是請Adapter進行調度。

想像一個畫面:
現在有一整排的騎兵,還有一整排的馬。
騎兵想騎馬,得聽從分隊長的命令。
「你,騎這匹!你,騎那匹!你,先等等,下一批馬才給你騎!」
分隊長跟馬、甚至跟騎兵不一定有直接關連。然而卻是讓騎兵能(有效率地)騎馬的關鍵。
(怎麼又是這麼奇怪的比喻)

如果你已經有使用過ListView顯示陣列資料的經驗,那你對Adapter應該並不陌生。
不過,使用RecyclerView的Adapter時會產生一個特別的東西:ViewHolder。

什麼是ViewHolder元件

ViewHolder,顧名思義,就是緩存View的元件。在上面的例子中,是那些馬匹的飼養員、盤子供應商。
講完了(欸)

ViewHolder之所以重要,是因為它使得後台無須調用FindViewByID()——你只需要在創建ViewHolder時綁定資料編號,就可以做很多事了。

ViewHolder要呈現在介面上需要透過LayoutManager調度。目前這個Manager不是很需要擔心,你只要知道它的存在就好了。

預備工作:ViewHolder

瞭解基礎概念後,就可以來寫code囉!
首先你要創建自定義的ViewHolder。它會決定你的資料呈現方式——至少,決定你的資料如何會被指派到介面上。
在以下範例中,我想要為ViewHolder準備一個基礎的文字顯示,以及提供一個簡單的點擊效果。
//將以下這段代碼貼到RecyclerView裡面
    class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
        TextView mTextView;
        ViewHolder(View itemView) {
            //TODO: 在這裡做指派動作(findViewById)
            super(itemView);
            mTextView = itemView.findViewById(R.id.txt_item_listview); //建議所有元件都放在同一layout,以免ViewHolder的資料沒有顯示在前端介面上
            itemView.setOnClickListener(this); //這裡是指下方的 onClick
        }

        @Override
        public void onClick(View v) {
            int clickedPos = getAdapterPosition();
            _OnClickListener.OnListItemClick(clickedPos);
        }
    }
    final private ListItemClickListener _OnClickListener; //TODO: 記得在Adapter建構子中設定/覆寫該listener
    public interface ListItemClickListener{
        void OnListItemClick(int clickedItemIndex);
    } //提供一個簡單的listener interface, 方便在其他地方(例如活動)進行覆寫

設定Adapter

接下來設定Adapter。
RecyclerView的Adapter有三項最重要、也是你一定要覆寫的函式
  1. onCreateViewHolder: 透過這個函式來指派要生成的項目View。
  2. onBindViewHolder: 核心中的核心,在此調度資料、並且把資料指派給View顯示。
  3. getItemCount: 用來告訴RecyclerView這次有多少資料要顯示。
只要把這三個寫完,你就已經完成一半以上的工作了。
以下提供範例寫法:
    //把以下代碼貼到Adapter類別中
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { //注意這裡的ViewHolder是自定義類別
        int layoutIdForListItem = R.layout.number_list_item; //TODO: 更換為自己的某頁Layout。注意該Layout要包含ViewHolder的指派元件,以免ViewHolder的更動沒有顯示在前端介面上
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        boolean shouldAttachToParentImmediately = false; //是否立刻加入到ViewGroup裡面
        View view = inflater.inflate(layoutIdForListItem, parent, shouldAttachToParentImmediately);
        return new ViewHolder(view);
    }
    /** Adapter中最重要的方法,在此同時進行資料查詢與資料指派。
     *
     * @param holder 針對的Holder
     * @param position 第幾項
     */
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        //TODO: 請自行創建或在Adapter建構子中指派_data資料,我的_data是List類別
        String message = _data.get(position);
        holder.mTextView.setText(message != null ? message : "null message."); //注意holder(ViewHolder)是自定義類別,裡面有一個mTextView
    }
    @Override
    public int getItemCount() {
        return _data.isEmpty() ? 0 : _data.size(); //這裡的_data是自定義的List變數
    }

如何設定與撰寫RecyclerView

當你有一個Adapter、也完成ViewHolder的設定之後,就可以來設定與串接RecyclerView了。
在前端拉好一個RecyclerView並指派ID後,就來到你的Activity/Fragment等任何主要的java腳本撰寫吧!

//以下代碼放置在你的Activity或Fragment java腳本。
//TODO: 注意若是在Fragment中,用"this"指派的Context會失效,請自行更換為getContext()

//此段代碼創建變數。可配合下方代碼自行修改成喜歡的名字
    private RecyclerViewAdapter recyclerAdapter; //TODO: 更換該類別為自訂的Adapter類別
    private RecyclerView recyclerView;
    public List<String> data;

//此段代碼指派初始資料。放置在OnCreate()函式中
        recyclerView = findViewById(R.id.rv_main); //TODO: 更換為自己的RecyclerView
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); //提供一個最基本的LayoutManager供RecyclerView使用
        linearLayoutManager.setStackFromEnd(true); //這樣會優先顯示清單最下方
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerAdapter = new RecyclerViewAdapter(this,data,this); //TODO: 更換為自己的建構子版本
        recyclerView.setAdapter(recyclerAdapter);
        recyclerView.setHasFixedSize(true); //這樣RecyclerView會比較認真地回收使用項目

//此段代碼是針對上面ViewHolder的自定義Listener進行覆寫。可以依照需求修改或者刪除。
//TODO: 對所在的activity或fragment 聲明(implement) 相關的Listener interface
    /***
     * 由RecyclerViewAdapter所使用。點擊項目就會觸發以下方法。
     * @param clickedItemIndex 點擊的項目編號。
     */
    @Override
    public void OnListItemClick(int clickedItemIndex) {
        ToastCustom.OnlyToast(this, "this is the " + clickedItemIndex + "th message: " + (!(data.get(clickedItemIndex)).equals("")?data.get(clickedItemIndex) : "(null message)."));
    }

結論與雜談

以上工作都完成的話,你的RecyclerView應該也能正常運作了。
幾個要點:
  • 要寫好RecyclerView,就要寫好Adapter。
  • 覆寫Adapter的三大函式:onCreateViewHolder、onBindViewHolder、getItemCount
  • 記得為ViewHolder創建單一的Layout,這樣方便前端UI設計、也方便後台引用
  • 個人建議:針對資料的引入,可以在Adapter建構時進行自定義指派、而非寫死在onBindViewHolder中。這樣一來可以節省效能又不出差錯(尤其結合SQL時)、二來也方便程式碼管理。
如果你對前端RecyclerView的顯示與自動更新有興趣,這裡再提供幾個關鍵字供參考:
  • (Adapter).notifyItemInserted / .notifyItemRangeInserted / .notifyItemRemoved / .notifyDataSetChanged: 讓Adapter去自動偵測變動的資料。如果有變動會自動更新給ViewHolder,從而讓RecyclerView更新。其中最後一個"DataSetChanged"是大絕,沒事別亂用!
  • (RecyclerView).scrollToPosition(int pos) 可以滑動RecyclerView到指定位置。配合清單長度查詢可以滑到最尾端!
  • (LayoutManager).setStackFromEnd / .setReverseLayout 可以客製化Layout Manager的顯示,其中前者是顯示時滾動到最下方的元件,後者則是反序顯示內含的元件。

參見這篇Adapter更新的問答: https://stackoverflow.com/questions/31367599/
與這篇LayoutManager設定的問答: https://stackoverflow.com/questions/35419985/

沒有留言:

張貼留言