0%

Flutter中如何优雅地使用弹框

 _2_.png

日常开发中,Flutter 弹框(Dialog)是我们使用频率非常高的控件。无论是提示用户信息、确认用户操作,还是表单填写,弹框都能派上用场。然而,看似简单的弹框,实际使用起来却有不少坑和使用的技巧。今天,我们就来聊聊这些弹框的使用技巧,文末还有关于在 bloc 如何使用弹框的内容,保证你看完之后干货满满,下面直接开始吧。

返回值的处理

用户在点击退出登录时,通常的做法就是弹框用来确认是否退出登录,返回值是 bool 类型,为 true 表示退出登录,反之不需要退出,这个时候应该怎处理这个 bool 类型的返回值呢?我们先来看看最常见的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
_showDialogWithCallBack(Function(bool) callBack){
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Logout"),
content: const Text("Are you sure you want to logout?"),
actions: [
TextButton(
onPressed: () {
callBack(false);
Navigator.of(context).pop();
},
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
callBack(true);
Navigator.of(context).pop();
},
child: const Text("Sure"),
)
],
);
},
);
}

点击按钮后调用 _showDialogWithCallBack 函数来显示弹框。

1
2
3
4
5
6
7
8
TextButton(
onPressed: () {
_showDialogWithCallBack((result) {
print("Result: $result");
});
},
child: Text('Dialog with callBack'),
)

除了上面这种使用回调函数传递结果,还有没有其它更加优雅的写法呢?我们知道 showDialog 本身返回的就是 Future 对象,如果需要在 Dialog 关闭后继续执行一些其它逻辑,我们可以使用 awaitasync 关键字来接收返回数据处理异步操作,下面来看看该怎么实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Future<bool?> _showAsyncDialog() {
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("Logout"),
content: const Text("Are you sure you want to logout?"),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text("Cancel"),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: const Text("Sure"),
)
],
);
},
);
}

利用 Navigator.of(context).pop(true) 方法来关闭对话框,并传递 true 或者 false 值作为对话框的返回结果,当点击按钮调用 _showAsyncDialog 弹出 Dialog

1
2
3
4
5
6
7
TextButton(
onPressed: () async {
bool? result = await _showAsyncDialog();
print("Result: $result");
},
child: Text('Async Dialog'),
)

在返回 result 之后再去处理其它的逻辑,相对于使用回调函数来处理结果,这种写法是不是更加的简洁,避免了回调函数那种代码嵌套,代码看起来也更清晰易懂吧。

如果 Dialog 中带有表单(如:TextField)该如何处理返回数据呢?其实和上面的处理方式是一样的,也可使用 asyncawait 来处理 showDialog 返回的 Future 对象,以返回用户输入的年龄(int 类型)为例,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Future<int?> _getAge(BuildContext context) {
final controller = TextEditingController();
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("How old are you?"),
content: TextField(
decoration: const InputDecoration(hintText: "e.g:22"),
keyboardType: TextInputType.number,
autofocus: true,
maxLength: 3,
controller: controller,
inputFormatters: [
TextInputFormatter.withFunction((oldValue, newValue) {
if (newValue.text.isEmpty) return newValue;
try {
final age = int.parse(newValue.text);
if (age > 120) return oldValue;
return newValue;
} catch (err) {
return oldValue;
}
})
]),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel")),
TextButton(
onPressed: () {
final age = int.parse(controller.text);
Navigator.of(context).pop(age);
},
child: const Text("Save"))
],
);
});
}

类似上面这种写法也同样适用于各种类型的 ActionSheet 返回值的处理。

自定义 Dialog 内容

上面的例子中 showDialogbuilder 函数返回内容都使用的是 AlertDialog 类,它属于 Material 设计风格的,而在我们平时开发的时候往往需要根据设计稿定制自己的 Dialog ,这个时候就需要自定义 Dialog 的内容了,如直接在 builder 函数中返回自定义的 widget 类,但是这个时候需要自己处理圆角、边框和阴影,还有处理点击对话框外部自动关闭对话框等功能。如果想要快速的实现一个自定义内容的弹框可以使用 Dialog 组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Dialog extends StatelessWidget {
const Dialog({
super.key,
this.backgroundColor,
this.elevation,
this.shadowColor,
this.surfaceTintColor,
this.insetAnimationDuration = const Duration(milliseconds: 100),
this.insetAnimationCurve = Curves.decelerate,
this.insetPadding = _defaultInsetPadding,
this.clipBehavior = Clip.none,
this.shape,
this.alignment,
this.child,
}) : assert(clipBehavior != null),
assert(elevation == null || elevation >= 0.0),
_fullscreen = false;

// .....
}

Dialog 小组价的构造函数可以看出来,其本身内置了背景色、阴影、边框、对其、边距和动画等属性,为对话框提供了默认样式和行为,定制自己的对话框时改起来也很方便。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
_showCustomDialog() {
showDialog(
context: context,
// 设置背景透明度
barrierColor: Colors.black.withOpacity(0.5),
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Container(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(height: 10),
Text('Custom Dialog Title', style: TextStyle(fontSize: 20)),
SizedBox(height: 16),
Text('This is a custom dialog content.'),
SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text('OK'),
),
SizedBox(height: 10),
],
),
),
);
},
);
}

bloc 中使用弹框

在使用 bloc(业务逻辑组件)进行状态管理时,很多的时候特定的业务逻辑执行过程中弹出对话框,如保存表单数据时,如果出现网络错误或验证错误,可以弹出对话框通知用户;执行删除操作时需要弹出对话框让用户确认操作是否继续;执行耗时操作弹出一个加载对话框,向用户展示操作正在进行,特别是在和硬件网关 zigbee 通信时,如查找设备,会有等待和重试的过程;以及完成某个操作时提示用户操作结果等等场景。但是我们在调用 showDialog 又需要传 context 参数,context 是用来标记 widgetwidget tree 的位置,bloc 中获取不到 context,那么在bloc 中使用弹框改如何实现呢?

还记得在《Flutter大型项目架构:路由管理篇》文章中实现的 AppNavigatorImpl 类吗?还有实现的 pushpop等操作,我们今天要实现在 bloc 弹框也是在 AppNavigatorImpl 类中实现。首先在其抽象类 AppNavigator 中添加如下代码:

1
2
3
4
5
6
7
8
9
10
abstract class AppNavigator {
// ...
Future<T?> showDialog<T extends Object?>(
AppPopupInfo appPopupInfo, {
bool barrierDismissible = true,
bool useSafeArea = false,
bool useRootNavigator = true,
});
// ...
}

AppNavigatorImpl 实现 showDialog 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@LazySingleton(as: AppNavigator)
class AppNavigatorImpl extends AppNavigator with LogMixin {
AppNavigatorImpl(
this._appRouter,
// 注入 PopupInfoMapper
this._appPopupInfoMapper,
this._appRouteInfoMapper,
);

TabsRouter? tabsRouter;

final AppRouter _appRouter;
// BasePopupInfoMapper 是抽象类, PopupInfoMapper 的父类
final BasePopupInfoMapper _appPopupInfoMapper;
final BaseRouteInfoMapper _appRouteInfoMapper;
final _popups = <AppPopupInfo>{};

StackRouter? get _currentTabRouter =>
tabsRouter?.stackRouterOfIndex(currentBottomTab);

StackRouter get _currentTabRouterOrRootRouter =>
_currentTabRouter ?? _appRouter;

BuildContext get _rootRouterContext =>
_appRouter.navigatorKey.currentContext!;

BuildContext? get _currentTabRouterContext =>
_currentTabRouter?.navigatorKey.currentContext;

BuildContext get _currentTabContextOrRootContext =>
_currentTabRouterContext ?? _rootRouterContext;

@override
int get currentBottomTab {
if (tabsRouter == null) {
throw 'Not found any TabRouter';
}
return tabsRouter?.activeIndex ?? 0;
}

@override
Future<T?> showDialog<T extends Object?>(
AppPopupInfo appPopupInfo, {
bool barrierDismissible = true,
bool useSafeArea = false,
bool useRootNavigator = true,
}) {
if (_popups.contains(appPopupInfo)) {
return Future.value(null);
}
_popups.add(appPopupInfo);

return showDialog<T>(
context: useRootNavigator
? _rootRouterContext
: _currentTabContextOrRootContext,
builder: (_) => m.WillPopScope(
onWillPop: () async {
_popups.remove(appPopupInfo);
return Future.value(true);
},
child: _appPopupInfoMapper.map(appPopupInfo, this),
),
useRootNavigator: useRootNavigator,
barrierDismissible: barrierDismissible,
useSafeArea: useSafeArea,
);
}
}

这里的 _rootRouterContextauto_route 的根 navigatorKey 提供的,用于管理和控制导航操作,这样可以实现在整个应用程序的任何地方访问和操作导航堆栈。auto_route 库通过 NavigatorGlobalKey 的结合实现了对 navigatorKey 的支持。_currentTabRouterContext 是当前所显示的 tabnavigatorKey 提供的,负责当前 tab 及子页面的导航操作。当调用 showDialog 时就可以使用 _rootRouterContext 或者 _currentTabContextOrRootContext 参数,这就解决了 context 参数的问题了。

AppPopupInfoAppRouteInfo 的作用是一样的,实现了抽象类 BasePopupInfoMapper,使用 @freezed 注解,在 AppPopupInfoMappermap 函数中使用 when 可根据不同工厂方法返回不同类型的 AppPopupInfo 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@freezed
class AppPopupInfo with _$AppPopupInfo {
const factory AppPopupInfo.confirmDialog({
@Default('') String message,
Function<void>? onPressed,
}) = _ConfirmDialog;
}

abstract class BasePopupInfoMapper {
Widget map(AppPopupInfo appRouteInfo, AppNavigator navigator);
}

@LazySingleton(as: BasePopupInfoMapper)
class AppPopupInfoMapper extends BasePopupInfoMapper {
@override
Widget map(AppPopupInfo appPopupInfo, AppNavigator navigator) {
return appPopupInfo.when(
confirmDialog: (message, onPressed) {
// 这里返回的是 Dialog 的内容
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Container(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(message),
SizedBox(height: 16),
TextButton(
onPressed: onPressed,
child: Text('OK'),
),
SizedBox(height: 10),
],
),
),
);
},
);
}
}

bloc 中或者页面中调用:

1
2
3
4
5
navigator.showDialog(AppPopupInfo.confirmDialog(
message: "显示弹框",
onPressed: () {
//...
}));

这里的 navigator 是从哪里来的呢?在《Flutter大型项目架构:路由管理篇》文章中最后一部分bloc 中使用 navigator 跳转页面介绍了将 navigator 注入到 BaseBlocBasePageState,感兴趣的可以去看看,这样无论是在 bloc 层还是 page 页面都能使用 navigator 跳转页面和弹出 Dialog 等操作,在 bloc 层使用 ActionSheetToast 等和 Dialog 一样的逻辑。

bloc 层使用弹框的实现过程其实也是路由管理的一部分,本篇文章是单独把弹框的使用做一个集锦,也希望能够帮到你。那么,在项目中你是如何使用弹框的呢,关于弹框的使用有什么好的建议和想法欢迎留言,感谢您的阅读,记得关注加点赞哦!