问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

Flutter弹框队列

发布网友 发布时间:2024-09-30 13:17

我来回答

1个回答

热心网友 时间:2024-10-04 09:36

概述

弹框队列在客户端是个比较常见的功能组件,用于统一管理APP内所有弹框的显隐,主要是解决如下图所示的“多个弹框同时弹出,其蒙层叠加带来的背景色加重的问题”

方案效果

将所有场景下的“即时弹框”转为相应的“弹框请求”按序入列,并逐个弹出,当下展示的弹框在消失后,唤起队列下一个弹框进行展示,如此反复直至队列为空。

              ?

具体来讲:

支持按序逐一弹框

支持按优先级弹框

支持弹框入列排重

保留FlutterNavigator弹框调用方式

对方案实现过程无感的同学,可直接使用pub.dev:dialog_queue

方案实现

Step1-队列元素DialogElement

APP各式弹框的抽象基类,是弹框队列的唯一管理元素,也代表了一个「弹框请求」

具体弹框样式及行为借成员变量onShow委托扩展子类实现

允许扩展子类在必要的时候重载update(DialogQueueElement?dialog)实现自己的数据更新

uniqueKey作为判断DialogElement相等性的唯一属性,在对象创建时如果未指定,则会使用一个随机uuid作为默认值

typedefonShow=FutureFunction();abstractclassDialogQueueElementextendsEquatable{onShowshow;//外部传入的展示业务对话框的回调方法lateint?_priority;lateString?_uniqueKey;lateString?_tag;lateString_uuid;DialogQueueElement(this.show,{int?priority=defaultPriority,String?uniqueKey,String?tag,}){_uuid=constUuid().v1();_priority=priority;_uniqueKey=uniqueKey??_uuid;_tag=tag;}intgetpriority=>_priority??defaultPriority;update(DialogQueueElement?dialog){if(dialog==null){return;}_show=dialog._show;_priority=dialog._priority??_priority;_uniqueKey=dialog._uniqueKey??_uniqueKey;_tag=dialog._tag??_tag;}showDialog(){returnshow.call();}@overrideStringtoString(){return'DialogQueueElement{tag:$_tag,priority:$_priority,uniqueKey:$_uniqueKey}';}@overrideList<Object?>getprops=>[_uniqueKey];}

Step2:添加DialogElement入列

谈具体实现前,我们必须知道「弹框入列-DialogQueue.addDialog()」这个动作是用来替换之前各个场景下的另一个动作:「即时弹框-showDialog」,调用方使用showDialog的方式方法在切换成addDialog之后,原则上应该保持一致,不去打破原本的使用机制。

我们拿Flutter官方的showModalBottomSheet底部弹框为例进行说明。一般来讲,业务调用方直接按下面的方式就能弹出一个官方底部对话框:

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);

而从showModalBottomSheet的源码来看,此方法其实是个Navigator的push操作,既然是pushpage操作,那表示它作为一个异步操作,会返回给调用方一个Future对象:

Future<T?>showModalBottomSheet<T>({requiredBuildContextcontext,requiredWidgetBuilderbuilder,...}){...finalNavigatorStatenavigator=Navigator.of(context,rootNavigator:useRootNavigator);//返回Future对象returnnavigator.push(_ModalBottomSheetRoute<T>(builder:builder,...));}

调用方拿到这个Future对象可能会做两个事情:

常规的异步转同步调用

等待对话框pop消失的时候,通过then方法回调做自己的业务逻辑。

在Navigatorpush的场景下,页面pop的时候会触发then回调

所以综上所述,在showDialog切换成addDialog之后,对于调用方而言,其原本的使用习惯应该维持不变。即addDialog方法体结构应该是这样:

Future<T?>addDialog<T>(DialogQueueElement<T>dialog){......returnxxxFuture;//如何定义此Future?}

那么这个xxxFuture从何而来?回顾之前DialogElement的定义,我们很自然会想到将委托给扩展子类的"typedefonShow=FutureFunction()"方法的执行结果作为xxxFuture进行返回,即:

Future<T?>addDialog<T>(DialogQueueElement<T>dialog){......returndialog.show.call();}

但很遗憾这并非是正确的方式,这样做只会导致当调用方调用addDialog的时候就立马触发其对话框的展示(show方法会被立即执行),然后调用方awaitaddDialog()等待的其实是“已展示的对话框”消失(pop)时的结果。

所以addDialog返回的Future对象,不应该指向调用方的show方法执行结果,那有没有办法既能在当下返回一个Future,并在恰当的时机控制Future的结果返回呢?有的,那就是FlutterCompleter。

我们应该借助Flutter的Completer做调用桥接。Future是一个异步计算的结果,而Completer是一个用来产生Future并控制计算过程结束时机的工具,用一个小例子展示下Completer的使用:

FutureopenImagePicker(){Completecompleter=newCompleter();ImagePicker.singlePicker(context,singleCallback:(data){//complete()表示成功收尾completer.complete(data);},failCallback:(err){//catchError()表示出错收尾completer.catchError(err);});//返回Completer的Futurereturncompleter.future;}

在上述例子中,我们可以任意时刻调用complete或catchError方法来结束openImagePicker的调用;甚至可以用来组装多个异步操作,并做最终的结束控制,可谓相当灵活。

所以在调用方执行addDialog的时候创建一个Completer,并返回completer.future,等待该对话框消失(pop)时让其对应的Completer执行complete()即可。可见一个DialogElement在队列中会有一个Completer与之对应。

特别注意:如果addDialog的时候DialogElement已存在,DialogQueue不会重复添加,而会更新已在队列中DialogElement的属性数据,并将已入列的DialogElement的Completer.future作为此次addDialog方法调用的返回值。也就是一个Completer.future可能会被多个调用方await;这样便能确保当Dialogpop(Completer.future.complete())的时候,多个调用方都能收到then的回调。

至此,对于「添加DialogElement入列」我们梳理一下:

DialogElement入列前需要查重(依据uniqueKey属性),做属性更新&Completer复用

DialogElement入列后需根据优先级重新排序

尝试展示下一个弹框

classDialogQueue{//标记当前是否有Dialog在展示bool_isShowing=false;//使用Map的方式存储DialogElement&Completer的映射关系finalMap<DialogQueueElement,Completer>_dialogQueue={};Future<T?>addDialog<T>(DialogQueueElementdialog){//1.入列前查重List<DialogQueueElement>keyList=_dialogQueue.keys.toList();intexistIndex=keyList.indexOf(dialog);if(existIndex>=0){DialogQueueElementcurrentDialog=keyList.elementAt(existIndex);Completer<T?>existCompleter=_dialogQueue[currentDialog]asCompleter<T?>;//1.1更新对话框数据currentDialog.update(dialog);//1.2更新排序_sortQueue();//1.3复用CompleterreturnexistCompleter.future;}Completer<T?>dialogCompleter=Completer();_dialogQueue[dialog]=dialogCompleter;//2.按优先级排序_sortQueue();//3.取下一个对话框进行展示_showNext();returndialogCompleter.future;}}

Step3:DialogQueue排序

按优先级排序:即priority越大则越优先弹出。由于DialogElement与Completer的映射关系存储于哈希表中,为了实现排序,另外定义了决定DialogElementList顺序的数组_sortedKeys,具体实现如下:

classDialogQueue{bool_isShowing=false;finalMap<DialogQueueElement,Completer>_dialogQueue={};//存储对话框顺序List<DialogQueueElement>_sortedKeys=[];//排序_sortQueue(){_sortedKeys=_dialogQueue.keys.toList();_sortedKeys.sort((a,b){if(a.priority>b.priority){return-1;}elseif(a.priority<b.priority){return1;}return0;});}}

Step4:DialogQueue弹窗时机

两个时机:

每当一个DialogElement入列时

每当一个DialogElement消失时

_showNext(){if(!_isShowing&&_dialogQueue.isNotEmpty){_isShowing=true;DialogQueueElementnextDialog=_sortedKeys.first;Completer?nextCompleter=_dialogQueue[nextDialog];_dialogQueue.remove(nextDialog);_sortedKeys.remove(nextDialog);//使用then监听对话框的消失nextDialog.showDialog().then((value){nextCompleter?.complete();//继续下一个弹框_isShowing=false;_showNext();});}}

至此,一个简易的DialogQueueforFlutter就算完整了,下面我们进入踩坑环节。

踩坑环节

踩坑1:在使用过程中发现了一种情况弹窗队列会直接报废,导致队列中余下的对话框都无法弹出。

还原下事发现场:

APP内从PageA跳PageB再跳PageC

在PageC此时发起若干个弹框请求入列

首个对话框弹出,点击对话框按钮执行Navigator.of(context).pushNamedAndRemoveUntil(PageA)

此时问题复现。咋回事?

先说结论:Flutternavigator执行pushNameAndRemoveUntil的时候把页面栈的历史元素直接remove,但未结束Route元素对应的future,导致正在展示的对话框的then回调得不到执行,继而DialogQueue中的_isShowing标识一直为true且无法调度下一个对话框的显示

nextDialog.showDialog().then((value){//?导致下方的逻辑都无法执行nextCompleter?.complete();_isShowing=false;_showNext();});

看看源码:flutter/lib/src/widgets/navigator.dart

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);0

而正常pop一个页面又是什么样的呢?

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);1

所以可见,同样是将页面移除,remove和pop有着本质的不同,pop会执行Completer.future.complete()继而触发then的回调,而remove是不会的。

方案一:监听Navigator路由动态变化

原理:当DialogQueue正在展示弹框时,将发生的didRemove行为及其目标pushRoute透传给业务方,由业务方来决定队列的下一步操作(清空队列或择机重弹)。也就是当DialogQueue当前正在展示的对话框被无情remove掉的时候,队列的按序弹框被强制中断,我们便允许业务方在合适的时候进行修复处理。

通过给我们业务的根WidgetMateralApp的navigatorObservers注入我们自定义的RouteObserver就能监听到页面被remove的情况。

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);2

方案二:代理NavigatorState的pushNameAndRemoveUntil方法

通过定义全局的NavigatorState替代Navigator.of(context)来执行页面导航的动作。如下:

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);3

两种方法都可行,只不过方案二的侵入性略高,但它的好处也是明显的,请看「踩坑2」。

踩坑2:弹框队列中的弹框元素持有过期的BuildContext,导致Navigator.of(context)为空

结合模型场景,我们看看问题出在哪:

初始页面栈:PageA>PageB>PageC(PageC在栈顶)

此时PageC收到多个弹框请求,构建多个DialogElement入列

相应的,在构建业务对话框的时候,我们往往会给对话框的取消按钮添加这么一句让对话框消失的代码:

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);4

注意!此时Navigator.of(context)的BuildContextcontext实例来源于Page3。

那么当Page3被pop或者remove的时候,context实例就算还存在于内存,但我们通过Navigator.of(context)试图获取的NavigatorState是为空的。也就意味着之前所构建好入列的业务对话框,其Navigator.of(context).pop()是无法执行的;对话框无法弹出消失也就意味着「弹框队列无法正常运转」。

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);5

所以,解决这个问题的方法,就是入列的对话框,使用NavigatorState的时候,不要依赖于当前Widgetbuild传入的context,而应该使用如「踩坑1」方案二所提及的全局NavigatorState

//XXXBusiness.dart业务模块弹框showModalBottomSheet(context:context,backgroundColor:Colors.white,builder:(BuildContextcontext){returnSomeWidget();},);6

踩坑3:弹框队列的按序弹出无法暂停,会打断用户的业务流程

如以下场景:用户在PageA的时候收到了多个弹框请求,处理NO.1对话框的时候,会触发PageB的跳转;NO.1对话框消失会自动触发NO.2对话框的弹出,继而遮挡住了PageB,打断了用户在PageB的业务流。

合理的做法是当用户跳转到PageB的时候,弹框队列停止工作,并在处理完业务回到PageA的时候,NO.2弹框再出现:

基于此,DialogQueue应该提供pause()&resume()方便用户随时暂停或恢复队列的执行;更进一步,应该封装一个这样的方法:允许在页面发生跳转时,暂停DialogQueue的执行,并在回到跳转前的Page时恢复DialogQueue的执行。具体怎么做,请大伙点击下方链接看源码即可。

至此,结合前面弹框队列设计的基本模型+上述踩坑的边界处理,便有了:

pub.dev:dialog_queue&emsp;github:flutter_dialog_queue

End

原文:https://juejin.cn/post/7099834211418243103
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
石璜镇历史演变 ...突突的声音,排气管还冒黑烟,有时走也是走走停停的, 我手机型号步步高V206的,手机的内存U盘显示以满但我查看了手机,手机里 ... 步步高v206,我想下载手机英语,希望各位指点指点,哪里有下载,下载后具体... 步步高V206B手机怎么下载java软件 如何通过化妆修饰不同脸型和鼻型? 最近我的手机总是莫名收到一条短信,说的SIM卡已被更换,这是怎么... 丈母娘和女婿什么相处模式最和谐? 女婿怎么跟岳父岳母搞好关系? 诛仙2中宠物怎样恢复到绑定前 小窗口关闭之前 禁止对母窗口进行操作 发发你2017到2019的男朋友九宫格都不够用吧是什么意思? 没朋友都是男朋友怎么回复对方 你找到这样的男朋友没我怎么回复着句话呢 朋友叫肖佳的藏头诗男性 怎么回复男朋友的性暗示? 梦见假雪委陵菜的预兆 原发性高血压最常见的死亡原因 原发性高血压最常见的死亡原因是 如何追求异地女孩子 ...8系统,电脑上一直有个腾讯QQ8.7什么的 怎么卸载都卸载不掉 湖湘文库:湖南历代文化世家·道州何氏卷内容简介 湖湘文库:湖南历代文化世家·道州何氏卷图书信息 湖湘文库·湖南历代文化世家、湘潭黎氏卷图书信息 黑鲨装机如何彻底删除(黑鲨装机u盘装系统) 1954年身份证有效期 Q345D无缝钢管规格参数 范子富的身份证510122195410274618 升级了系统小米2手机默认的主题壁纸换了?咋样换回来-自带的那副山水图... 正宗的玉米粑粑该怎么做? 如何不显示代码效果显示文本 上海迪士尼对游客的着装有要求吗?有何注意事项? 迪士尼对穿着有要求吗迪士尼对穿着有什么要求 日本迪士尼对游客的着装有要求吗?有何注意事项? 迪士尼对穿着的具体要求有哪些? 苏州锐丰建声灯光音响器材工程安装有限公司产品展示 链家又开始隐藏成交记录了! 怎样发布一个微信公众号? 如何查二手房成交价格 如何查房子成交价格 会同话“长尾巴”什么意思? 带亲戚家孩子去游乐场玩,孩子把手机丢了,你该怎么办? 如何解决孩子手机丢了怎么办的难题? 三星怎样把联系人导入到手机里啊 家里养鱼养几条鱼旺财养鱼一般养几条为好? 农行异地取款手续费收费标准是多少 三生花、七世梦。是什么意思 三生七世是什么意思? 三生七世的介绍 想问问大家怎么找工作的?用智联、猎聘、boss直聘、拉勾还是前程...