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);
@override
Widget 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});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false, // 禁用调试标签
home: BaseWidget(),
);
}
}
// MARK: 根组件 用来处理公用逻辑
class BaseWidget extends StatefulWidget {
const BaseWidget({super.key});
@override
State<BaseWidget> createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
@override
void 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);
}
}
};
}
@override
void dispose() {
LoadingView().hide();
super.dispose();
}
@override
Widget 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");