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 哈,这里提供额外一种思路。

  • 相关阅读:
    Cygwin配置总结
    javap 指令集
    超好用的Vim配置
    超过 130 个你需要了解的 vim 命令
    21、面向对象
    20、MySQLdb
    深入java字符串原理及其效率分析
    数据库执行计划
    SQL中EXISTS的用法
    mybatis之foreach用法
  • 原文地址:https://www.cnblogs.com/nesger/p/11715796.html
Copyright © 2011-2022 走看看