zoukankan      html  css  js  c++  java
  • Android 列表(ListView、RecyclerView)不断刷新最佳实践

    本文微信公众号「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 哈,这里提供额外一种思路。

  • 相关阅读:
    联想 Vibe Shot(Z90-3) 免recovery 获取ROOT权限 救砖 VIBEUI V3.1_1625
    联想 Z5S(L78071)免解锁BL 免rec 保留数据 ROOT Magisk Xposed 救砖 ZUI 10.5.370
    联想 Z5(L78011) 免解锁BL 免rec 保留数据 ROOT Magisk Xposed 救砖 ZUI 10.5.254
    联想 S5 Pro(L78041)免解锁BL 免rec 保留数据 ROOT Magisk Xposed 救砖 ZUI 5.0.123
    第二阶段 冲刺八
    第二阶段 冲刺七
    第二阶段 冲刺六
    第二阶段 冲刺五
    代码大全阅读笔记03
    学习进度十二
  • 原文地址:https://www.cnblogs.com/nesger/p/11715796.html
Copyright © 2011-2022 走看看