1.自定义样式
2.自定义LoadingView
import 'package:flutter/material.dart';enum LoadingStyle {onlyIndicator, // 仅一个转圈等待roundedRectangle, // 添加一个圆角矩形当背景maskingOperation, // 添加一个背景蒙层, 阻止用户操作
}class LoadingView {static final LoadingView _singleton = LoadingView._internal();factory LoadingView() {return _singleton;}LoadingView._internal();OverlayEntry? _overlayEntry; void show(BuildContext context, {LoadingStyle type = LoadingStyle.onlyIndicator}) {if (_overlayEntry == null) {_overlayEntry = _createOverlayEntry(type);Overlay.of(context).insert(_overlayEntry!); }}void hide() {_overlayEntry?.remove();_overlayEntry = null;}OverlayEntry _createOverlayEntry(LoadingStyle type) => OverlayEntry(builder: (BuildContext context) {List<Widget> stackChildren = [];if (type == LoadingStyle.roundedRectangle) {stackChildren.add(Center(child: Container(width: 100,height: 100,padding: const EdgeInsets.all(20.0),decoration: BoxDecoration(color: Colors.white,borderRadius: BorderRadius.circular(8.0),),child: const Align(alignment: Alignment.center,child: SizedBox(width: 45,height: 45,child: CircularProgressIndicator(color: Colors.black,),),),),),);} else if (type == LoadingStyle.maskingOperation) {stackChildren.addAll([const Opacity(opacity: 0.5,child: ModalBarrier(dismissible: false, color: Colors.black),),const Center(child: CircularProgressIndicator()),]);} else {stackChildren.add(const Center(child: CircularProgressIndicator()),);}return Stack(children: stackChildren);},);
}
3.自定义ToastView
import 'package:flutter/material.dart';enum ToastPosition { center, bottom }class ToastView {static OverlayEntry? _overlayEntry;static void showToast(BuildContext context,String message, {ToastPosition position = ToastPosition.center,int second = 2,Color backgroundColor = Colors.white,Color textColor = Colors.black,double horizontalMargin = 16,EdgeInsetsGeometry padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10),}) {_overlayEntry?.remove();_overlayEntry = OverlayEntry(builder: (context) => ToastWidget(message: message,position: position,backgroundColor: backgroundColor,textColor: textColor,horizontalMargin: horizontalMargin,padding: padding,),);Overlay.of(context)?.insert(_overlayEntry!);Future.delayed(Duration(seconds: second), () {_overlayEntry?.remove();_overlayEntry = null;});}
}class ToastWidget extends StatelessWidget {final String message;final ToastPosition position;final Color backgroundColor;final Color textColor;final double horizontalMargin;final EdgeInsetsGeometry padding;const ToastWidget({Key? key,required this.message,required this.position,required this.backgroundColor,required this.textColor,required this.horizontalMargin,required this.padding,}) : super(key: key);@overrideWidget build(BuildContext context) {return Positioned(top: position == ToastPosition.center ? MediaQuery.of(context).size.height / 2 : null,bottom: position == ToastPosition.bottom ? 50.0 : null,left: horizontalMargin,right: horizontalMargin,child: Material(color: Colors.transparent,child: Align(alignment: position == ToastPosition.center ? Alignment.center : Alignment.bottomCenter,child: FittedBox(fit: BoxFit.scaleDown,child: Container(padding: padding,decoration: BoxDecoration(color: backgroundColor,borderRadius: BorderRadius.circular(8),),child: Text(message,textAlign: TextAlign.center,style: TextStyle(fontSize: 15,color: textColor,),),),),),),);}
}
4.创建一个全局的回调管理AlertCallbackManager
经过上面自定义视图,我们注意到,视图的展示都需要BuildContext context。若是这样的话,就强行将弹窗视图的逻辑绑定到了具体的某个组件上,导致组件销毁时弹窗也必须销毁。否则,context都消失了,你又如何去处理插入其中的视图?
我们往往需要,让等待转圈在离开页面后还继续展示,让Toast在关闭页面时也不被影响到其提示的时长。
所以,这里我们用了一个全局回调管理。
enum AlertCallbackType { none, showLoading, hideLoading, showToast }class AlertCallbackManager {// 私有构造函数AlertCallbackManager._privateConstructor();// 单例实例static final AlertCallbackManager _instance = AlertCallbackManager._privateConstructor();// 获取单例实例的方法static AlertCallbackManager get instance => _instance;// 定义闭包类型的回调函数Function(AlertCallbackType type, String message)? callback;
}
5.创建一个根组件,将等待加载和Toast提示当作公共逻辑来处理。
有了全局回调管理,我们还需要有一个不会被轻易销毁的根组件,来提供BuildContext context。
注意:全局提示回调, 要放在MaterialApp包装之后,因为这里的LoadingView实现方式需要放在MaterialApp之下。
void main() async {WidgetsFlutterBinding.ensureInitialized();runApp(const MyApp());
}// MARK: 用来包装MaterialApp
class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return const MaterialApp(title: 'Flutter Demo',debugShowCheckedModeBanner: false, // 禁用调试标签home: BaseWidget(),);}
}// MARK: 根组件 用来处理公用逻辑
class BaseWidget extends StatefulWidget {const BaseWidget({super.key});@overrideState<BaseWidget> createState() => _BaseWidgetState();
}class _BaseWidgetState extends State<BaseWidget> {@overridevoid initState() {super.initState();// 提示回调, 要放在MaterialApp包装之后AlertCallbackManager.instance.callback = (type, message) async {if (mounted) { // 检查当前State是否仍然被挂载(即没有被dispose)if (type == AlertCallbackType.showLoading) {LoadingView().show(context);} else if (type == AlertCallbackType.hideLoading) {LoadingView().hide();} else if (type == AlertCallbackType.showToast) {ToastView.showToast(context, message);}}};}@overridevoid dispose() {LoadingView().hide();super.dispose();}@overrideWidget build(BuildContext context) {return const HomePage();}
}
然后在需要展示的地方,用如下方式调用,最好再进一步将方法封装得短一些。
AlertCallbackManager.instance.callback?.call(AlertCallbackType.showLoading, "");AlertCallbackManager.instance.callback?.call(AlertCallbackType.hideLoading, "");AlertCallbackManager.instance.callback?.call(AlertCallbackType.showToast, "message");