1. 概述
1.1 关于Widgets Easier
本库是一个 Flutter 组件库,旨在提供用于Flutter开发的组件,使得开发者能够更简单地构建出更丰富地界面效果。项目地址为:
1.2 模块安装
在你的Flutter项目中,运行下面的命令:
flutter pub add widgets_easier
即可安装最新版本的 Widgets Easier 库。
2. 什么是承若型对话
2.1 基本概念
承若型对话是一套带有异步性质的弹窗机制。这种弹窗机制能够广泛运用于处理登录验证等具有未来性质。
一个 Future 对象在其生命周期中有两种状态:
- 未完成(Uncompleted):
当 Future 被创建时,它处于未完成状态。这意味着异步操作尚未完成,结果还不可用。
- 完成(Completed):
异步操作完成后,Future 进入完成状态。这个状态有两种可能的结果:
- 成功(Fulfilled): 异步操作成功完成,Future 获得了一个值。
- 失败(Rejected): 异步操作因错误而未能成功完成,Future 获得了一个错误。
与之对应的,在承若型对话中注重的时过程,也即是更关注 Uncompleted 到 Completed 的过度状态和状态结果。这就是说,在一组承若型对话中至少有三个对话单元(Dialog),分别是
- 加载中;
- 加载完成;
- 加载成功
- 加载失败
3. 案例:用户登录
以用户登录为例,承若型对话可以被设计为以下三个阶段的对话单元:
- 加载中(Loading):
当用户提交登录信息(如用户名和密码)后,应用会显示一个加载中的对话框或提示。这个阶段对应于 Future 的 未完成(Uncompleted) 状态,表示登录的异步操作正在进行中,结果尚未确定。
- 加载完成(Completion):
这个阶段对应于 Future 的 完成(Completed) 状态,根据操作的结果,可以进一步分为两个子阶段:
- 加载成功(Success):
如果登录验证成功,应用会显示一个成功的提示对话框或者直接跳转到应用的主界面。这表示异步操作成功完成,用户被成功验证。
- 加载失败(Failure):
如果登录失败(例如,因为提供了错误的凭证或服务器无响应),应用会显示一个错误提示对话框。这个对话框通常会提供错误信息,并可能允许用户重试或进行其他恢复操作。
这种承若型对话机制不仅提高了用户体验,通过明确显示每个阶段的状态,还帮助用户理解应用的当前状态和可能的下一步操作。例如,在登录过程中:
用户输入凭证并点击“登录”按钮。
系统立即响应,显示一个模态加载中对话框,通知用户“正在验证,请稍候…”。
根据异步验证结果,加载中对话框会被替换为:
成功对话框,显示“登录成功!欢迎回来。”然后可能会自动关闭对话框并导航到主界面。
失败对话框,显示“登录失败,请检查您的用户名和密码后重试。”并提供重试或找回密码的选项。
通过这种方式,承若型对话为异步操作提供了清晰和直观的用户反馈,增强了交互的连贯性和用户的信任感。
4. 使用Widgets Easier库实现
使用 Widgets Easier库实现一个承若型对话单元特别简单你只需要准备好你的验证UI和校验函数,对话的事情可以完全交给 Widgets Easier库来实现。
4.1 使用FutureDialogs类
FutureDialogs 类用于处理承若型弹窗,通过静态方法 FutureDialogs.show
调用弹窗。
FutureDialogs.show 方法需要配置一个异步函数futureCallback和两个 Dialog 模板,一个是 SuccessDialog、另外一个是FailureDialog,如果有需要也可以指定第三个Dialog模板 LoadingDialog。其中:
异步函数futureCallback表示一个具有两个结果的未来事件,它可能成功或者失败;
futureCallback的结果遵循一定规范,返回结果的类型为Future<Map<String, dynamic>>;
这个Map中,包含status和data两个字段;
status的类型为bool,表示成功或者失败;
data表示是一个字符串,表示成功或者失败时相关消息。
futureCallback还未实现(fulfill)的过程中,展示的是 LoadingDialog;
如果futureCallback实现(fulfill)后的而己过为成功,则展示 SuccessDialog;
反之则展示FailureDialog。
4.2 构建一个SuccessDialog
现在我们构建一个
import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; class SuccessDialog extends StatelessWidget { final String data; const SuccessDialog( this.data, { super.key, }); @override Widget build(BuildContext context) { return Align( alignment: Alignment.center, child: Material( color: Colors.transparent, child: Container( width: 200, height: 200, margin: const EdgeInsets.all(26), padding: const EdgeInsets.all(26), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.check, color: Colors.green, size: 60, ), const Gap(20), Text(data, style: const TextStyle(color: Colors.white)), ], ), ), ), ); } }
4.3 构建一个FailureDialog
import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; class FailureDialog extends StatelessWidget { final String data; const FailureDialog( this.data, { super.key, }); @override Widget build(BuildContext context) { return Align( alignment: Alignment.center, child: Material( color: Colors.transparent, child: Container( width: 230, height: 200, margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.close, color: Colors.red, size: 60, ), const Gap(20), Text(data, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, )), ], ), ), ), ); } }
4.4 登录页面
import 'package:example/login/dialogs/failure_dialog.dart'; import 'package:flutter/material.dart'; import 'package:widgets_easier/widgets_easier.dart'; import 'dialogs/success_dialog.dart'; import 'login_controller.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); final LoginController loginManager = LoginController(); void handleLogin(BuildContext context) { // 从文本控制器获取用户名和密码 final String username = usernameController.text; final String password = passwordController.text; // 显示一个异步操作的对话框,这个对话框将在 simulateLogin 方法的 Future 完成后关闭 FutureDialogs.show<String>( context: context, futureCallback: () => loginManager.simulateLogin(username, password), buildSuccessDialog: (data) { return SuccessDialog(data); }, buildFailureDialog: (data) { return FailureDialog(data); }, ).then((result) { // 检查从 simulateLogin 返回的结果 if (result != null && result['status'] == true) { // 如果登录成功,导航到主页 Navigator.pushReplacementNamed(context, '/login-success'); } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('FutureDialogs 示例')), body: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ TextField( controller: usernameController, decoration: const InputDecoration( labelText: '输入你的账号', border: OutlineInputBorder(), prefixIcon: Icon(Icons.account_circle), ), ), const SizedBox(height: 10), TextField( controller: passwordController, decoration: const InputDecoration( labelText: '输入你的密码', border: OutlineInputBorder(), prefixIcon: Icon(Icons.lock), ), obscureText: true, ), const SizedBox(height: 20), SemanticButton( text: '登录', type: SemanticEnum.primary, onTap: () => handleLogin(context), radius: 40, isOutlined: true, ) ], ), ), ); } }
其中:
FutureDialogs.show<String>( context: context, futureCallback: () => loginManager.simulateLogin(username, password), buildSuccessDialog: (data) { return SuccessDialog(data); }, buildFailureDialog: (data) { return FailureDialog(data); }, ).then((result) { // 检查从 simulateLogin 返回的结果 if (result != null && result['status'] == true) { // 如果登录成功,导航到主页 Navigator.pushReplacementNamed(context, '/login-success'); } });
可以我们在完成完成之后还可以链式执行一些操作。这种设计非常有必要,以此例子为例,在登录成功之后我们跳转到了登录成功页面。其中这里then的参数result也是futureCallback返回的值。另一方面,如果有需要也可以在这里继续下一个弹窗。
4.5 异步函数
import 'dart:async'; class LoginController { Future<Map<String, dynamic>> simulateLogin( String username, String password, ) async { // 一般对于空密码等情况可以在客户端验证,以减少API请求 if (username.isEmpty || password.isEmpty) { return { 'status': false, 'data': '账户名或密码不能为空', }; } // 模拟请求API返回结果,有可能成功也有可能失败 else if (username == 'jclee95' && password == '123456') { await Future.delayed(const Duration(seconds: 1)); return {'status': true, 'data': '登录成功'}; } else { await Future.delayed(const Duration(seconds: 1)); return {'status': false, 'data': '账户名或密码错误'}; } } }
在这段代码中,simulateLogin是一个模拟异步登录的函数。
如果登录成功,则返回:
{'status': true, 'data': '登录成功'}
如果登录失败则返回:
{'status': false, 'data': '账户名或密码错误'}
4.6 登录成功页面
import 'package:flutter/material.dart'; class LoginSuccessView extends StatelessWidget { const LoginSuccessView({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('登录成功'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Icon( Icons.check_circle_outline, size: 100, color: Colors.green, ), const Padding( padding: EdgeInsets.all(8.0), child: Text( '您已成功登录!', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black, ), ), ), ElevatedButton( onPressed: () { Navigator.pushReplacementNamed(context, '/home'); }, child: const Text('回到主页'), ), ], ), ), ); } }
4.7 效果展示
用户名密码为空:
输入错误的密码:
密码认证成功
5. 弹窗动画
5.1 FutureDialogs.zoomIn方法
在之前的内容中,我介绍了FutureDialogs.show的基本用法,这个方法没有动进入画效果。另外有一个 FutureDialogs.zoomIn方法拥有完全一样的接口,但是拥有从小到大的动画效果,看起来就像这样:
5.2 自定义动画
如果有需要,你可以使用 FutureDialogs.showFutureDialog 静态方法自定义动画。该方法有一个transitionBuilder
参数接受一个RouteTransitionsBuilder,可以用于指定动画。例如之前弹窗部分代码修改为:
FutureDialogs.showFutureDialog( context: context, transitionBuilder: (context, animation, secondaryAnimation, child) { return AnimateStyles.rollIn(animation, child); }, futureCallback: () => loginManager.simulateLogin(username, password), buildSuccessDialog: (data) { return SuccessDialog(data); }, buildFailureDialog: (data) { return FailureDialog(data); }, ).then((result) { // 检查从 simulateLogin 返回的结果 if (result != null && result['status'] == true) { // 如果登录成功,导航到主页 Navigator.pushReplacementNamed(context, '/login-success'); } });
注:这里使用的AnimateStyles.rollIn
动画需要单独安装:
flutter pub add flutter_easy_animations
效果看起来是这样的:
F. 报告问题和贡献代码
你可以在这个项目的 GitHub 上提供反馈或报告问题。如果你觉得这个库缺少某个功能,请创建一个功能请求。欢迎提交拉取请求。