上周组内项目 安排在应用内显示一个toast的弹窗提醒 由于是flutter项目,其框架本身所提供的toast组件并不好用,UI上也不支持自定义的设计。因此决定自己写一个toast组件。虽然是一个很小的组件,但是前前后后也折腾了快两个礼拜,也算是正式上手flutter开发后的第一次实践吧。
由于toast出现的时间是不定的,因此它将以绝对定位的方式插入页面之中。所以使用了 overlay
这个类,该类的具体介绍这里就不展开叙述了。Overlay 主要就是两个方法,1. 往Overlay中插入entry,2. 删除Overlay中的entry。
具体实现如下
static Future _show(BuildContext context, {@required String msg}) {
OverlayEntry entry = new OverlayEntry(
builder: (BuildContext context) => Positioned(
//top值,可以改变这个值来改变toast在屏幕中的位置
top: MediaQuery.of(context).size.height * 1 / 30,
child: Container(
alignment: Alignment.center,
width: MediaQuery.of(context).size.width,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.0),
child: _buildToastWidget(context, msg),
)),
));
///往Overlay中插入插入OverlayEntry
Overlay.of(context).insert(entry);
///两秒后,移除Toast
Future result = Future.delayed(Duration(seconds: 1)).then((value) {
entry.remove();
});
_lastToast = result;
return result;
}
//toast UI绘制
static _buildToastWidget(context, String msg) {
return Container(
width: MediaQuery.of(context).size.width * 0.8,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
shape: BoxShape.rectangle,
color: Colors.white,
border: Border.all(color: Colors.black, width: 2),
),
child: Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: Text(msg, style: TextStyle(color: Colors.black, decoration: TextDecoration.none, fontSize: 20)),
),
);
}
这样实现的toast在使用时只需要传入context(上下文)便可以在页面上的指定位置插入一个我们所需要的toast了。
等等,是不是觉得功能还有些欠缺,点一次出来一个toast的用户体验貌似不太好。那加一个处理重复点击的函数吧
//处理重复多次点击
static void _handleDuplicateAndShow(String message, int type, BuildContext context, int duration) {
if (_lastMsg == message) {
//相同信息内容
int currentms = DateTime.now().millisecondsSinceEpoch;
int interval = currentms - _lastShowms;
if (interval > MIN_INTERVAL) {
//大于时间间隔 可以显示
_show(context, msg);
_lastShowms = currentms;
}
} else {
_show(context, msg);
_lastMsg = message;
}
}
看起来仿佛是大功告成了,只需要向外暴露出相关的接口,便可以供开发者自由使用了。
持续封装
/// 提示
static void showInfo(String message, {BuildContext context, int duration}) {
//调用
}
/// 警告
static void showWarning(String message, {BuildContext context, int duration}) {
//
}
/// 成功
static void showSuccess(String message, {BuildContext context, int duration}) {
//
}
/// 错误
static void showError(String message, {BuildContext context, int duration}) {
//
}
到这里了,相应的功能实现了,接口也对外暴露了。需求基本上实现了,但是代码的结构粗略看来还是很乱的。leader建议修改 ,使用MVVM来对这个toast进行改造。
那首先用toast这个类来复习一下MVVM的设计模式吧。
由于需要对Toast的设计具有可扩展性,因此拆分出 ToastWidget 作为整个设计中的 View部分,使用ToastManager作为viewModel来管理整个toast的显示,toast这个类则是model层,负责用数据去实例化一个toast。
建立目录结构如上。开始进行实现。
由于toast出现的场景、时间具有很强的不确定性,我们无法判断其所在的具体上下文。这里最开始考虑使用dart中的Stream流的机制,对toast事件创建一个事件流,并进行监听。
StreamController<Toast> _toastController = StreamController<Toast>();
Stream<Toast> get toastStream => _toastController.stream;
但是流的机制存在的问题便是,我们无法对流中的事件进行缓存处理,即当业务需求为 一个个展示toast,当前toast动画执行未结束时不展示下一个,会发现很难做到这点。因此,最后的选择方案为 使用 Queue进行事件的保存与展示。
我们首先在Manager中定义一个队列
static Queue<Toast> toastQueue = Queue();
当队列中有任务在执行(即 有toast在展示时,下一个任务等待,在当前展示完成后的回调中去获取下一个展示的toast事件。)
static void _handleOneByOneShow(String message, int type, BuildContext context, int duration) {
toastQueue.add(Toast(message, type, duration));
if (isShowing) {
//当前toast仍在显示
return;
} else {
isShowing = true;
//当前无toast显示 队列不为空 递归调用
_recursiveShow(toastQueue, context);
}
}
//递归的函数
static void _recursiveShow(Queue toastQueue, BuildContext context) {
if (toastQueue.isNotEmpty) {
Toast firstToast = toastQueue.first;
firstToast.show(context).whenComplete(() =>{
toastQueue.removeFirst(),
_recursiveShow(toastQueue, context)
}
);
} else {
isShowing = false;
return;
}
}