日常开发中,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
关闭后继续执行一些其它逻辑,我们可以使用 await
和 async
关键字来接收返回数据处理异步操作,下面来看看该怎么实现。
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
)该如何处理返回数据呢?其实和上面的处理方式是一样的,也可使用 async
和 await
来处理 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
内容 上面的例子中 showDialog
的 builder
函数返回内容都使用的是 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
是用来标记 widget
在 widget tree
的位置,bloc
中获取不到 context
,那么在bloc
中使用弹框改如何实现呢?
还记得在《Flutter大型项目架构:路由管理篇》文章中实现的 AppNavigatorImpl
类吗?还有实现的 push
、pop
等操作,我们今天要实现在 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, this ._appPopupInfoMapper, this ._appRouteInfoMapper, ); TabsRouter? tabsRouter; final AppRouter _appRouter; 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, ); } }
这里的 _rootRouterContext
是 auto_route
的根 navigatorKey
提供的,用于管理和控制导航操作,这样可以实现在整个应用程序的任何地方访问和操作导航堆栈。auto_route
库通过 Navigator
和 GlobalKey
的结合实现了对 navigatorKey
的支持。_currentTabRouterContext
是当前所显示的 tab
的navigatorKey
提供的,负责当前 tab
及子页面的导航操作。当调用 showDialog
时就可以使用 _rootRouterContext
或者 _currentTabContextOrRootContext
参数,这就解决了 context
参数的问题了。
AppPopupInfo
和 AppRouteInfo
的作用是一样的,实现了抽象类 BasePopupInfoMapper
,使用 @freezed
注解,在 AppPopupInfoMapper
的 map
函数中使用 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) { 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
注入到 BaseBloc
和 BasePageState
,感兴趣的可以去看看,这样无论是在 bloc
层还是 page
页面都能使用 navigator
跳转页面和弹出 Dialog
等操作,在 bloc
层使用 ActionSheet
、Toast
等和 Dialog
一样的逻辑。
在 bloc
层使用弹框的实现过程其实也是路由管理的一部分,本篇文章是单独把弹框的使用做一个集锦,也希望能够帮到你。那么,在项目中你是如何使用弹框的呢,关于弹框的使用有什么好的建议和想法欢迎留言,感谢您的阅读,记得关注加点赞哦!