Android 列表(ListView、RecyclerView)不斷刷新最佳實踐

  • 2019 年 10 月 21 日
  • 筆記

本文微信公眾號「AndroidTraveler」首發。

背景

在 Android 列表開發過程中,有時候我們的 Item 會有一些組件,比如倒計時。這類組件要求不斷刷新,這個時候由於列表復用的機制,因此會有一些坑。那麼我們本篇文章就給大家講兩個主題。

第一個是列表復用是否一定有問題。
第二個是出現問題有哪些解決方案可供我們選擇。

小 Demo

由於我們的主題重點是為了解決不斷刷新問題,因此關於 RecyclerView 的基本使用就不再贅述,不清楚的小夥伴可以看下我之前的文章:
RecyclerView基本使用

首先我們看下效果圖:

很簡單,就是一個 RecyclerView 列表,列表項有兩個組件。分別代表第幾項和剩餘秒數。

這裡就是通過倒計時來演示刷新可能存在的問題。

重點程式碼是 Adapter 裡面的顯示邏輯,初始為:

@Override  public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {      holder.mTvNum.setText(String.valueOf(position + 1));      updateTime(holder, itemList.get(position));  }    private void updateTime(final RecyclerViewViewHolder holder, final long time) {      String content;      long remainTime = time - System.currentTimeMillis();      remainTime /= 1000;      if (remainTime <= 0) {          content = "Time up";          holder.mTxtTitle.setText(content);          return;      }        content = "剩下"+remainTime+"秒";      holder.mTxtTitle.setText(content);  }

全部程式碼見:https://github.com/nesger/RecyclerView/tree/feature/refresh

接下來我們增加刷新方法,有很多種,我們一一說明。

1. 使用 handler 來實現倒計時刷新

修改顯示程式碼,如下:

@Override  public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {      holder.mTvNum.setText(String.valueOf(position + 1));      updateTime(holder, itemList.get(position));  }    private void updateTime(final RecyclerViewViewHolder holder, final long time) {      String content;      long remainTime = time - System.currentTimeMillis();      remainTime /= 1000;      if (remainTime <= 0) {          content = "Time up";          holder.mTxtTitle.setText(content);          return;      }        content = "剩下"+remainTime+"秒";      holder.mTxtTitle.setText(content);      holder.mTxtTitle.postDelayed(new Runnable() {          @Override          public void run() {              updateTime(holder, time);          }      }, 1000);  }

可以看到通過 handler 延時一秒,然後每次更新時間也是減少一秒。

我們看下效果圖:

可以看到沒滾動之前還好,滾動之後會發現,倒計時都亂了。

當然有時候可能不會暴露出來,比如滾動數目少,或者只有部分組件有倒計時,不像我們這個例子,所有項目都有倒計時,但是這也間接留下了可能的坑。

出現這個問題的原因在於組件的復用,如果你用 ListView 演示,並且不用復用,那麼是不會錯亂的。

當然列表不復用這個肯定是不推薦的。

因此,該方式不推薦

全部程式碼見:https://github.com/nesger/RecyclerView/tree/feature/refresh_1

2. 使用 Timer 來實現倒計時刷新

@Override  public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {      holder.mTvNum.setText(String.valueOf(position + 1));      updateTime(holder, itemList.get(position));  }    private void updateTime(final RecyclerViewViewHolder holder, final long time) {      String content;      long remainTime = time - System.currentTimeMillis();      remainTime /= 1000;      if (remainTime <= 0) {          content = "Time up";          holder.mTxtTitle.setText(content);          return;      }        content = "剩下"+remainTime+"秒";      holder.mTxtTitle.setText(content);  }

一樣不行,不推薦

全部程式碼見:https://github.com/nesger/RecyclerView/tree/feature/refresh_2

3. 使用 Timer + View 集合

其實我們簡單分析一下就知道,出現上面錯亂情況的原因大致是兩個:一個是復用,一個是程式碼多次調用。
所以如果能夠解決這兩個問題,那麼這個問題就解決了。

因為我們這裡的業務是倒計時監聽,所有 View 都是一樣的,就是一秒更新一次。

所以我們的定時器不需要 N 個,只需要一個,在構造函數初始化即可。

另外為了避免復用和程式碼多次調用問題,我們將 View 通過一個集合保存起來。

最後修改的程式碼如下:

private Timer mTimer;  private Set<RecyclerViewViewHolder> mHolders;    public RecyclerViewAdapter(Activity activity, List<Long> itemList) {      if (activity == null || itemList == null) {          throw new IllegalArgumentException("params can't be null");      }      this.activity = activity;      this.itemList = itemList;      mHolders = new HashSet<>();      mTimer = new Timer();      mTimer.scheduleAtFixedRate(new TimerTask() {          @Override          public void run() {              for (RecyclerViewViewHolder holder : mHolders) {                  updateTime(holder, holder.getTime());              }          }      }, 0, 1000);  }    @Override  public void onBindViewHolder(RecyclerViewViewHolder holder, int position) {      holder.setTime(itemList.get(position));      mHolders.add(holder);      holder.mTvNum.setText(String.valueOf(position + 1));      updateTime(holder, itemList.get(position));  }

效果圖如下:

可以看到沒問題了。

當然這裡有些優化還沒處理,因為本篇主要是思路分析,這裡就不添加了。

待優化點:定時器的啟動和關閉跟生命周期關聯,無數據源不啟用定時器等。

全部程式碼見:https://github.com/nesger/RecyclerView/tree/feature/refresh_3

該方法來自與一名朋友的分享。

4. 使用 ScheduledExecutorService + View 集合

這邊 AndroidStudio 有安裝阿里巴巴提供的一個程式碼檢測插件,鏈接為:https://plugins.jetbrains.com/plugin/10046-alibaba-java-coding-guidelines

在 AndroidStudio 輸入插件名字 Alibaba Java Coding Guidelines 查找安裝即可。

在方法 3 使用 Timer 時提示下面資訊:

Run multiple TimeTask by using ScheduledExecutorService rather than Timer because Timer will kill all running threads in case of failing to catch exceptions.    //org.apache.commons.lang3.concurrent.BasicThreadFactory  ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,      new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());  executorService.scheduleAtFixedRate(new Runnable() {      @Override      public void run() {          //do something      }  },initialDelay,period, TimeUnit.HOURS);

所以我們這裡修改 Timer 為 ScheduledExecutorService:

private ScheduledExecutorService mExecutorService;    public RecyclerViewAdapter(Activity activity, List<Long> itemList) {      if (activity == null || itemList == null) {          throw new IllegalArgumentException("params can't be null");      }      this.activity = activity;      this.itemList = itemList;      mHolders = new HashSet<>();      mExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {          @Override          public Thread newThread(@NonNull Runnable r) {              Thread thread = new Thread(r);              thread.setName("countdown");              return thread;          }      });      mExecutorService.scheduleAtFixedRate(new Runnable() {          @Override          public void run() {              for (RecyclerViewViewHolder holder : mHolders) {                  updateTime(holder, holder.getTime());              }          }      }, 0, 1000, TimeUnit.MILLISECONDS);  }

全部程式碼見:https://github.com/nesger/RecyclerView/tree/feature/refresh_4

有更多方法歡迎到上面的 GitHub 鏈接提 PR,可以基於 feature/refresh 分支新建分支。

有另外一位朋友提出了自定義 View 的處理方式,將倒計時的功能放到 View 裡面去處理,這個感興趣的小夥伴可以實現然後提 PR 哈,這裡提供額外一種思路。