记录一次APP弹窗排队的实践

667 阅读5分钟

前言

2022年11月11日。很吉利的日子,APP功能日益复杂,在首页的的弹窗就超过4个,分别是首页的引导,APP的更新弹窗,APP的协议更新弹窗,APP的广告弹窗,权限申请弹窗等。因为有些弹窗是通过dialog 去实现的,当网络较慢的情况下,就会出现一个问题,APP都切换到其他板块去了,dialog 却弹出来了。于是我们打算优化这一流程。

正文

我们先对业务诉求进行分析。

  • 我们需要将这些弹窗固定在首页弹起。
  • 弹窗的弹起是有一定的顺序的。
  • 这些弹窗部分依赖于一个网络接口,
  • 有的却是本地参数判断。

设计

定义需要获取到外部参数:

依次弹起,我们可以分析下,可以使用一个队列存储。因为队列是先进先出的嘛。在首页弹起的功能,我们可以获取到首页的生命周期对象 lifecycleOwner,有些弹窗是dialogFragment 实现的,所以需要一个fragmentManger,然后就是上下文了,同时我们期望外部可以接受到我们队列处理完成的监听。而这些都可以通过一个队列管理器去存储他。那么我们需要的参数就大致是这些:

     FragmentManager fragmentManager;
    LifecycleOwner lifecycleOwner;
    Consumer<Boolean> queueEndConsumer;

定义队列对象操作的接口

我们对也是诉求中的‘‘这些弹窗部分依赖于一个网络接口,有的却是本地参数判断’’,因为本地数据是可以主线程获取到,我们可以在添加到队列中之前判断是否添加到队列,然后是网络请求部分,网络请求时异步的,所以我们需要定义一个接口去标记他准备好了,为了使用简单化,我们将处理完成和不需要弹起弹窗的逻辑混为一谈,不需要弹起弹窗的逻辑就包含了,网络错误,网络成功后判断不需要弹,登录过期等等。

public interface ViewCallBack {
    /**
     * 准备完成
     */
    void ready(int position);
​
    /**
     * 结束操作。
     */
    void endHandle(int position);
}

为啥要返回一个position,在我的预想里面,是通过排序队列去做的,而不是通过数组顺序。实际上我却是用的数组顺序,通过排序队列是便于后期迭代,比如这期的登录过期弹窗就没有处理,我们登录过期弹窗是接口返回的,虽然也可以强行调用一个必须登录的接口判断是否登录过期,但是感觉怪怪的。

定义队列对象

队列对象需要负责提供是否添加到队列中,需要回调上面的操作接口,需要处理网络请求,本地逻辑,需要处理弹窗等等。

public abstract class ViewQueueItem {
​
    protected int position;
    protected ViewCallBack callBack;
    protected FragmentManager fragmentManager;
    protected LifecycleOwner lifecycleOwner;
    /**
     * 是否准备好了。
     */
    protected Boolean ready=false;
    /**
     * 是否已经完成
     */
    protected Boolean endHandle=false;
​
    public Boolean getEndHandle() {
        return endHandle;
    }
​
    public void setEndHandle(Boolean endHandle) {
        this.endHandle = endHandle;
    }
​
    public Boolean getReady() {
        return ready;
    }
​
    public void setReady(Boolean ready) {
        this.ready = ready;
    }
​
    public void setLifecycleOwner(LifecycleOwner lifecycleOwner) {
        this.lifecycleOwner = lifecycleOwner;
    }
​
    public void setFragmentManager(FragmentManager fragmentManager) {
        this.fragmentManager = fragmentManager;
    }
​
    /**
     * 有一些逻辑上不需要处理的。比如说APP 第一次安装启动。
     * 更新弹窗就不需要处理。协议更新弹窗也不需要处理。
     *
     * @return needHandle
     */
    public abstract boolean needHandle();
​
    /**
     * 设置监听回调
     *
     * @param callBack callBack
     */
    void setCallBack(ViewCallBack callBack) {
        this.callBack = callBack;
    }
​
    /**
     * 开始准备
     */
    public abstract void gettingReady();
​
​
    /**
     * 开始操作
     */
    public abstract void startHandle();
​
​
    /**
     * 获取到当前队列的位置。
     *
     * @param position position
     */
    void setPosition(int position) {
        this.position = position;
    }
}

可以看到,我依旧把下标丢进去了。

调用对象的设计

我们上面需要在首页显示的时候,才显示弹窗,所以我们单纯的使用MutableLiveData 即可。

队列管理器设计

在添加完队列之后,需要调用一个开始队列。我们将添加的class与真实的队列分开。开始的时候,开启一个循环,判断是否添加到队列中,然后在循环中设置一些队列实现类的基本参数,包括position(在我的设计中,position 是需要外部传入的,但是架不住砍需求)。

当一个队列对象准备好的时候,获取并判断队列头部的对象是否准备好,如果准备好了就调用显示方法,如果没有准备好,就继续等等,当一个对象通过逻辑判断认为他结束的时候,我们将他从队列中移除出去。

这么有一个需要注意的点就是,需要判断队列头部对象是否频繁调用显示函数。

完整代码:

public class ViewQueueManger extends ViewModel implements Observer<ViewQueueItem> {
    FragmentManager fragmentManager;
    LifecycleOwner lifecycleOwner;
    Consumer<Boolean> queueEndConsumer;
    /**
     * 这个用来存储之前的队列。
     */
    List<ViewQueueItem> queueItems = new ArrayList<>();
​
    MutableLiveData<ViewQueueItem> queueLive = new MutableLiveData<>();
    /**
     * 这个是排序队列。
     */
    PriorityQueue<ViewQueueItem> queue = new PriorityQueue<>(10, (o1, o2) -> o1.position - o2.position);
​
    public ViewQueueManger addQueue(ViewQueueItem queueItem) {
        queueItems.add(queueItem);
        return this;
    }
    public ViewQueueManger setFragmentManager(FragmentManager fragmentManager) {
        this.fragmentManager = fragmentManager;
        return this;
    }
​
    public ViewQueueManger setLifecycleOwner(LifecycleOwner lifecycleOwner) {
        this.lifecycleOwner = lifecycleOwner;
        queueLive.observe(lifecycleOwner, this);
        return this;
    }
​
​
​
    public ViewQueueManger setQueueEndConsumer(Consumer<Boolean> queueEndConsumer) {
        this.queueEndConsumer = queueEndConsumer;
        return this;
    }
​
​
​
    /**
     *
     */
    public void start() {
        int index = 0;
        for (ViewQueueItem item : queueItems) {
            // 设置下标。
            if (item.needHandle()) {
                item.setPosition(index);
                item.setLifecycleOwner(lifecycleOwner);
                item.setFragmentManager(fragmentManager);
                // 直接丢进队列。
                LogUtils.e("添加进入队列:" + item.getClass().getName());
                queue.add(item);
                index++;
                item.setCallBack(new ViewCallBack() {
                    @Override
                    public void ready(int position) {
                        item.setReady(true);
                        LogUtils.e("ready:" + position + "  " + item.getClass().getName());
                        doQueue();
                    }
​
                    @Override
                    public void endHandle(int position) {
                        item.setEndHandle(true);
                        LogUtils.e("endHandle:" + position + "  " + item.getClass().getName());
                        // 移除当前完成的本身
                        if (queue.contains(item)) {
                            queue.remove(item);
                            LogUtils.e("移除已经结束的内容" + item.getClass().getName());
                        }
                        // 然后获取下一个。
                        doQueue();
​
                    }
                });
                // 开始准备
                item.gettingReady();
            }
        }
    }
​
    private void doQueue() {
        if (queue.size() > 0) {
            // 获取头部的数据。如果准备好了就操心。
            ViewQueueItem element = queue.element();
            // 准备完成,并且没有操作完。
            if (element.getReady() && !element.getEndHandle()) {
                if (null == queueLive.getValue()) {
                    queueLive.postValue(element);
                    LogUtils.e("发送展示要求:" + element.getClass().getName());
                } else if (queueLive.getValue() != null && queueLive.getValue() != element) {
                    queueLive.postValue(element);
                    LogUtils.e("发送展示要求:" + element.getClass().getName());
                }
            }
        }else {
            LogUtils.e("---------------");
            if (null!=queueEndConsumer){
                queueEndConsumer.accept(true);
            }
        }
    }
​
​
    @Override
    public void onChanged(ViewQueueItem viewQueueItem) {
        // 接受到需要显示的消息。
        viewQueueItem.startHandle();
    }
}

总结

其实,这就是对于队列的一种单纯的使用,这种想法萌生已久,需要注意的是ViewQueueItem的实现类必须逻辑严谨,必须调用准备好了或者结束的函数。

写完后才发现,这个调调 不仅仅是处理界面,可以处理一系列需要排队的诉求,也可以把权限相关的加进来。