【Flutter】Form表单,狗都不用到真香 & 如何自定义Form表单子控件

【Flutter】Form表单,狗都不用到真香 & 如何自定义Form表单子控件

技术博客 admin 27 浏览

Form表单的使用、封装、与自定义

前言

不管是一个应用还是一个网页,一个表单提交都是少不了,登录、注册、完善消息等常用的功能都离不开表单的提交。

在之前我们开发 Android 应用都是直接堆布局就能实现表单,校验逻辑一一对应一个一个写,没什么问题,甚至我开发过前端网页也是这么做的,也没什么问题,既然如此,我为什么要用 Form 这个鬼东西。

看似我们又需要学习一个新的控件和一些规则,其实它真的很简单。它简化了校验,错误展示,重置,等一系列操作,让表单更加的方便,统一化的操作真的用了之后真香。

Form表单其实不用学,特别简单,就是对一系列的输入进行统一的操作,如输入内容校验、输入框重置以及输入内容保存等。

Form的子孙是 FormField 类型,FormState 为 Form 的 State 类,可以通过 Form.of() 或 GlobalKey 获得,关于 Form 的一些用法和常用方法都是一些基本的固定用法,一点都不难。

下面就一起学习如何使用 Form ,怎么封装 Form 内的输入框,怎么自定义其他的子 Form 表单。

一、如何简单使用

在没有 Form 表单之前,我们都是直接用 TextField ,我们对 TextField 做一些封装,直接使用,设置 TextEditingController FocusNode 然后通过操作输入框和焦点,来控制校验,通过设置错误文本并刷新布局来展示错误信息。

dart
复制代码
Map<String, Map<String, dynamic>> formData = { 'phone': { 'value': '', 'controller': TextEditingController(), 'focusNode': FocusNode(), 'hintText': '请输入手机号码'.tr, 'obsecure': false, }, 'code': { 'value': '', 'controller': TextEditingController(), 'focusNode': FocusNode(), 'hintText': '请输入验证码'.tr, 'obsecure': false, }, };
less
复制代码
//电话号码 _buildInputLayout( "phone", Assets.authLoginPhoneIcon, leftIconWidth: 13.5, leftIconHeight: 16, marginTop: 20, textInputType: TextInputType.phone, textInputAction: TextInputAction.next, errorText: controller.mobilePhoneErrorText, onSubmit: (formKey, value) { state.formData[formKey]!['focusNode'].unfocus(); FocusScope.of(context).requestFocus(state.formData['code']!['focusNode']); }, ), //验证码 _buildInputLayout( "code", Assets.authLoginVerifyIcon, leftIconWidth: 13.5, leftIconHeight: 15, marginTop: 10, paddingRight: 15, textInputAction: TextInputAction.next, errorText: controller.codeErrorText, showRightIcon: true, rightWidget: MyTextView( controller.isCounting ? controller.countdownTime.toString() + " s" : '获取验证码'.tr, textAlign: TextAlign.center, textColor: controller.isCounting ? ColorConstants.gray99 : ColorConstants.appBlue, fontSize: 14, paddingRight: 3, isFontMedium: true, onClick: controller.isCounting ? null : () => controller.showVerifyCodedDialog(), ).paddingOnly(top: 15, bottom: 15), onSubmit: (formKey, value) { state.formData[formKey]!['focusNode'].unfocus(); controller.doChangePhone(); }, ),

内部的 TextField 的封装之前有贴过代码,这里不重复。

我们需要手动的通过 TextEditingController 拿到值, 通过 FocusNode 设置这个 Form 表单内部的焦点变化到下一步。

ini
复制代码
/// 执行手机号码的绑定 void doChangePhone() { mobilePhoneErrorText = null; codeErrorText = null; update(); var phoneController = state.formData['phone']!['controller']; var codeController = state.formData['code']!['controller']; phone = phoneController.text; code = codeController.text; Log.d('phone:$phone code:$code'); if (Utils.isEmpty(phone)) { mobilePhoneErrorText = "电话号码不能为空"; update(); } else if (Utils.isEmpty(code)) { codeErrorText = "验证码不能为空"; update(); } else { _requestForgetPsd(); } } // =========================== 焦点控制 =========================== FocusNode? _phoneFocusNode; FocusNode? _codeFocusNode; void _onPhoneFocusChange() { if (_phoneFocusNode?.hasFocus == true) { mobilePhoneErrorText = null; update(); } } void _onCodeFocusChange() { if (_codeFocusNode?.hasFocus == true) { codeErrorText = null; update(); } } @override void onInit() { super.onInit(); _phoneFocusNode = state.formData['phone']!['focusNode']; _codeFocusNode = state.formData['code']!['focusNode']; } @override void onReady() { super.onReady(); _phoneFocusNode?.addListener(_onPhoneFocusChange); _codeFocusNode?.addListener(_onCodeFocusChange); } @override void onClose() { super.onClose(); _phoneFocusNode?.removeListener(_onPhoneFocusChange); _codeFocusNode?.removeListener(_onCodeFocusChange); _phoneFocusNode = null; _codeFocusNode = null; }

使用 Form 表单之前的效果:

使用 Form 表单之后,我们就可以不需要这么多控制器,焦点控制的逻辑,我们就能使用 Form 表单比较简单的实现:

less
复制代码
return Form( key: _formKey, // 关联 GlobalKey child: Column( children: [ TextFormField( // 设置表单字段的验证规则 validator: (value) { if (value?.isEmpty == true) { return '请输入姓名'; } return null; }, onChanged: (value) { setState(() { _name = value; }); }, decoration: const InputDecoration( labelText: '姓名', ), ), // _buildInputLayout( // "name", // Assets.authLoginPhoneIcon, // leftIconWidth: 13.5, // leftIconHeight: 16, // marginTop: 20, // textInputType: TextInputType.phone, // textInputAction: TextInputAction.next, // onSaved: (value) { // state.formData['name']!['value'] = value; // }, // ), TextFormField( // 设置表单字段的验证规则 validator: (value) { if (value?.isEmpty == true) { return '请输入邮箱'; } return null; }, onChanged: (value) { setState(() { _email = value; }); }, decoration: const InputDecoration( labelText: '邮箱', ), ), // _buildInputLayout( // "email", // Assets.authLoginPasswordIcon, // leftIconWidth: 13.5, // leftIconHeight: 16, // marginTop: 20, // textInputType: TextInputType.phone, // textInputAction: TextInputAction.next, // onSaved: (value) { // state.formData['email']!['value'] = value; // }, // ), SizedBox(height: 16.0), ElevatedButton( onPressed: () { // 验证表单字段 if (_formKey.currentState?.validate() == true) { //可以手动的设置错误文本 state.formData['name']!['errorText'] = null; state.formData['email']!['errorText'] = null; controller.update(); _formKey.currentState?.save(); //调用保存 // 表单验证通过,可以提交表单 _submitForm(); } else { Log.e("校验不通过"); } }, child: const Text('提交'), ), ElevatedButton( onPressed: () { setState(() { _selectedOption = null; }); _formKey.currentState?.reset(); //重置 }, child: const Text('重置'), ), ], ), ); void _submitForm() { // 在这里执行表单提交的逻辑,例如发送网络请求等 print("当前的表单数据为:name:${state.formData['name']!['value']} email:${state.formData['email']!['value']}"); }

我们就能很简单得到实现 Form 表单,它的优势就是集中的快速校验,展示错误信息,重置等操作。

二、如何封装表单

是的 Form 的子控件想要实现快速校验,展示错误信息,重置,保存等操作,就需要让其子控件集成自 FormField 对象,而 Flutter 自带几个 FormField 的实现对象

我们关注的重点就是输入框,我使用 TextField 习惯了,使用 Form 中的子对象 TextFormField 有什么区别?

其实没什么区别,只是多了一些 validator,onSaved等给 Form 父布局调用的一些方法。在其他的用法上其实都是一样的,甚至我们都能简化一些 TextFormField 的封装:

kotlin
复制代码
/* * Form表单内部的输入框的封装 */ class MyTextFormField extends StatelessWidget { String formKey; String value; bool? enabled; TextInputType inputType; String? labelText; TextStyle? labelStyle; String? errorText; double cursorWidth; Color? cursorColor; String? hintText; String? initialValue; TextStyle? hintStyle; TextStyle? style; bool? autofocus; int? maxLines = 1; InputBorder? border; BoxBorder? boxBorder; bool? showLeftIcon; Widget? leftWidget; bool? showRightIcon; Widget? rightWidget; bool? showDivider; Color? dividerColor; bool obscureText; double height; Color? fillBackgroundColor; double? fillCornerRadius; EdgeInsetsGeometry padding; EdgeInsetsGeometry margin; InputDecoration? decoration; TextInputAction textInputAction = TextInputAction.done; Function? onChanged; Function? onSubmit; String? Function(String? value)? onSaved; //Form表单的保存 String? Function(String? value)? validator; //Form表单的校验 final ClickType changeActionType; //默认没有点击类型 final int changeActionMilliseconds; //点击类型的时间戳(毫秒) final ClickType submitActionType; //默认没有点击类型 final int submitActionMilliseconds; //点击类型的时间戳(毫秒) MyTextFormField( this.formKey, this.value, { Key? key, this.enabled = true, //是否可用 this.inputType = TextInputType.text, //输入类型 this.initialValue, //初始化文本 this.labelText, this.labelStyle, this.errorText, //错误的文本 this.cursorWidth = 2.0, // 光标宽度 this.cursorColor = ColorConstants.appBlue, // 光标颜色 this.hintText, //提示文本 this.hintStyle, //提示文本样式 this.style, //默认的文本样式 this.autofocus = false, // 自动聚焦 this.maxLines = 1, //最多行数,高度与行数同步 this.border = InputBorder.none, //TextFiled的边框 this.boxBorder, // 外层Container的边框 this.showLeftIcon = false, //是否展示左侧的布局 this.leftWidget, //左侧的布局 this.showRightIcon = false, //是否展示右侧的布局 this.rightWidget, //右侧的布局 this.showDivider = true, // 是否显示下分割线 this.dividerColor = const Color.fromARGB(255, 212, 212, 212), // 下分割线颜色 this.obscureText = false, //是否隐藏文本,即显示密码类型 this.height = 50.0, this.fillBackgroundColor, //整体的背景颜色 this.fillCornerRadius, //整体的背景颜色圆角 this.padding = EdgeInsets.zero, //整体布局的Padding this.margin = EdgeInsets.zero, //整体布局的Margin this.decoration, //自定义装饰 this.textInputAction = TextInputAction.done, //默认的行为是Done(完成) this.validator, //Form验证 this.onSaved, //Form保存 this.onChanged, //输入改变回调 this.onSubmit, //完成行为的回调(默认行为是Done完成) this.changeActionType = ClickType.none, //默认没有点击类型 this.changeActionMilliseconds = 500, //回调类型的时间戳(毫秒) this.submitActionType = ClickType.none, //默认没有点击类型 this.submitActionMilliseconds = 500, //回调类型的时间戳(毫秒) }) : super(key: key); @override Widget build(BuildContext context) { //抽取的改变的回调 changeAction(value) { onChanged?.call(formKey, value); } //抽取的提交的回调 submitAction(value) { onSubmit?.call(formKey, value); } return Container( margin: margin, decoration: BoxDecoration( color: fillBackgroundColor ?? Colors.transparent, borderRadius: BorderRadius.all(Radius.circular(fillCornerRadius ?? 0)), border: boxBorder, ), padding: padding, child: ConstrainedBox( constraints: BoxConstraints(minHeight: height), child: Column( mainAxisAlignment: maxLines == null ? MainAxisAlignment.start : MainAxisAlignment.center, children: [ TextFormField( enabled: enabled, style: style, maxLines: maxLines, keyboardType: inputType, obscureText: obscureText, cursorWidth: cursorWidth, cursorColor: DarkThemeUtil.multiColors(cursorColor, darkColor: ColorConstants.white), autofocus: autofocus!, validator: validator, decoration: decoration ?? InputDecoration( hintText: hintText, hintStyle: hintStyle, icon: showLeftIcon == true ? leftWidget : null, border: border, suffixIcon: showRightIcon == true ? rightWidget : null, labelText: labelText, errorText: errorText, errorStyle: const TextStyle(color: Colors.red, fontSize: 11.5), errorBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.red), ), ), onChanged: changeActionType == ClickType.debounce ? debounce(changeAction, changeActionMilliseconds) : changeActionType == ClickType.throttle ? throttle(changeAction, changeActionMilliseconds) : changeAction, onFieldSubmitted: submitActionType == ClickType.debounce ? debounce(submitAction, submitActionMilliseconds) : submitActionType == ClickType.throttle ? throttle(submitAction, submitActionMilliseconds) : submitAction, onSaved: onSaved, textInputAction: textInputAction, ), showDivider == true ? Divider( height: 0.5, color: dividerColor!, ).marginOnly(top: errorText == null ? 0 : 10) : const SizedBox.shrink(), ], ), ), ); } //带参数的函数防抖,由于参数不固定就没有用过扩展,直接用方法包裹 void Function(String value) debounce(void Function(String value) callback, [int milliseconds = 500]) { Timer? _debounceTimer; return (value) { if (_debounceTimer?.isActive ?? false) _debounceTimer?.cancel(); _debounceTimer = Timer(Duration(milliseconds: milliseconds), () { callback(value); }); }; } //带参数的函数节流,由于参数不固定就没有用过扩展,直接用方法包裹 void Function(String value) throttle(void Function(String value) callback, [int milliseconds = 500]) { bool _isAllowed = true; Timer? _throttleTimer; return (value) { if (!_isAllowed) return; _isAllowed = false; callback(value); _throttleTimer?.cancel(); _throttleTimer = Timer(Duration(milliseconds: milliseconds), () { _isAllowed = true; }); }; } }

使用:

less
复制代码
return Form( key: _formKey, // 关联 GlobalKey child: Column( children: [ _buildInputLayout( "name", Assets.authLoginPhoneIcon, leftIconWidth: 13.5, leftIconHeight: 16, marginTop: 20, textInputType: TextInputType.phone, textInputAction: TextInputAction.next, onSaved: (value) { state.formData['name']!['value'] = value; }, ), _buildInputLayout( "email", Assets.authLoginPasswordIcon, leftIconWidth: 13.5, leftIconHeight: 16, marginTop: 20, textInputType: TextInputType.phone, textInputAction: TextInputAction.next, onSaved: (value) { state.formData['email']!['value'] = value; }, ), SizedBox(height: 16.0), ElevatedButton( onPressed: () { // 验证表单字段 if (_formKey.currentState?.validate() == true) { //可以手动的设置错误文本 state.formData['name']!['errorText'] = null; state.formData['email']!['errorText'] = null; controller.update(); _formKey.currentState?.save(); //调用保存 // 表单验证通过,可以提交表单 _submitForm(); } else { Log.e("校验不通过"); } }, child: const Text('提交'), ), ElevatedButton( onPressed: () { setState(() { _selectedOption = null; }); _formKey.currentState?.reset(); //重置 }, child: const Text('重置'), ), ], ), ); Widget _buildInputLayout( String key, String leftIconRes, { double leftIconWidth = 0, double leftIconHeight = 0, double marginTop = 23, double paddingRight = 18, bool? showRightIcon = false, //是否展示右侧的布局 Widget? rightWidget, //右侧的布局 TextInputType textInputType = TextInputType.text, String? errorText, TextInputAction textInputAction = TextInputAction.done, String? Function(String? value)? validator, //自定义Form验证 String? Function(String? value)? onSaved, //Form的保存 Function? onSubmit, }) { return IgnoreKeyboardDismiss( child: MyTextFormField( key, state.formData[key]!['value'], hintText: state.formData[key]!['hintText'], hintStyle: const TextStyle( fontSize: 14.0, fontWeight: FontWeight.w500, ), margin: EdgeInsets.only(left: 20, right: 20, top: marginTop), showDivider: false, fillBackgroundColor: DarkThemeUtil.multiColors(ColorConstants.white, darkColor: ColorConstants.darkBlackItem), fillCornerRadius: 5, padding: EdgeInsets.only(left: 16, right: paddingRight, top: 2.5, bottom: 2.5), height: 50, style: TextStyle( color: DarkThemeUtil.multiColors(ColorConstants.tabTextBlack, darkColor: ColorConstants.white), fontSize: 14.0, fontWeight: FontWeight.w500, ), inputType: textInputType, textInputAction: textInputAction, onSubmit: onSubmit, validator: validator ?? state.formData[key]!['validator'], onSaved: onSaved, cursorColor: ColorConstants.tabTextBlack, obscureText: state.formData[key]!['obsecure'], errorText: errorText ?? state.formData[key]!['errorText'], showLeftIcon: true, showRightIcon: showRightIcon, rightWidget: rightWidget, leftWidget: Row( children: [ MyAssetImage(leftIconRes, width: leftIconWidth, height: leftIconHeight), const Spacer(), Container( color: ColorConstants.graye5, width: 1, height: 15, ) ], ).constrained(width: 30), ), ); }

和之前一样的封装,UI效果和之前是一样的效果,只是我们不再需要一些 Contriller FocusNode 等控制逻辑了,个人感觉会更方便了。

此时我们的 FormFiledState 只需要设置一些简单的表单数据即可。

kotlin
复制代码
Map<String, Map<String, dynamic>> formData = { 'name': { 'value': '', 'hintText': '请输入姓名', 'errorText': null, 'obsecure': false, 'validator': (value) { if (value.isEmpty) { return '请输入姓名'; } return null; }, }, 'email': { 'value': '', 'hintText': '请输入邮箱', 'errorText': null, 'obsecure': false, 'validator': (value) { if (value.isEmpty) { return '请输入邮箱'; } return null; }, }, };

那么除了默认的,常见的输入框控件,Form表单还有其他的控件怎么办?

三、如何自定义子控件

除了最常见的表单输入框还有一些常见的下拉选控件,现在 Flutter 也支持下拉选控件 DropdownButtonFormField 。

它的用法他也是比较类似:

less
复制代码
Container( margin: const EdgeInsets.only(left: 20, right: 20, top: 23), decoration: BoxDecoration( color: DarkThemeUtil.multiColors(ColorConstants.white, darkColor: ColorConstants.darkBlackItem), borderRadius: const BorderRadius.all(Radius.circular(5)), border: Border.all(color: ColorConstants.secondaryAppColor, width: 0.5), ), padding: const EdgeInsets.only(left: 16, right: 18, top: 2.5, bottom: 2.5), child: DropdownButtonFormField<String>( decoration: const InputDecoration( hintText: "请选择来源", hintStyle: TextStyle( fontSize: 14.0, fontWeight: FontWeight.w500, ), //左侧图片 icon: null, border: InputBorder.none, //右图片 suffixIcon: null, //框内,文本上面的提示文本 labelText: null, errorText: null, //错误信息 errorStyle: TextStyle(color: Colors.red, fontSize: 14), ), value: _selectedOption, style: TextStyle( color: DarkThemeUtil.multiColors(ColorConstants.tabTextBlack, darkColor: ColorConstants.white), fontSize: 15.0, fontWeight: FontWeight.w500, ), items: resource.map((String option) { return DropdownMenuItem<String>( value: option, child: Text(option), //如果想要图片加文本,在这里修改布局即可 ); }).toList(), onChanged: (value) {}, onSaved: (value) { setState(() { _selectedOption = value; }); }, validator: (value) { if (value == null) { return 'Please select an option'; } return null; }, ), ),

我们还是用同样的样式来修饰这个下拉选,效果为:

那么如果我这是一个很复杂的长表单呢?还想要一些其他的控件呢?比如 CheckBox,RadioButton,Switch,Image 等控件呢?我都想让他们自动校验并且展示对应的错误信息提示呢?

没关系,我们可以自定义 FormField 。

比如我们定义一个 CheckBoxFormField ,一般我只需要定义 onSaved,validator,错误文本的显示逻辑,如下:

php
复制代码
class CheckBoxFormField extends FormField<bool> { CheckBoxFormField({ FormFieldSetter<bool>? onSaved, FormFieldValidator<bool>? validator, bool? initialValue = false, bool? autovalidate = false, required Widget title, }) : super( onSaved: onSaved, validator: validator, initialValue: initialValue ?? false, autovalidateMode: autovalidate! ? AutovalidateMode.always : AutovalidateMode.disabled, builder: (FormFieldState<bool> field) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Checkbox( value: field.value, onChanged: field.didChange, ), title, ], ), if (field.hasError) //自定义错误信息 Text( field.errorText!, style: const TextStyle(color: Colors.red, fontSize: 14), ).marginOnly(left: 15), ], ); }, ); }

通过一个简单的定义,我们就能在 Form 中使用了,效果如下:

那么问题来了,如果是其他的控件想要在 Form 中使用,应该如何继承封装,我相信现在你应该懂了吧。

这个非得你自己去实现不可呀,因为我不能把你要用的每一个控件都给封装一遍吧,并且每一个项目的错误展示也不一致,还是需要你自行实现哦。

总结

之前我写 Flutter 的表单的时候也是一个输入框一个输入框,一个下拉选一个下拉选,一个控件一个控件的堆叠,写一大堆的焦点控制,一大堆的输入框控制器等逻辑。如果是下拉选,各种控制器与数据切换逻辑。

虽然都可以自己细节化的控制,但是真的没必要,用了 Form 之后真的清爽很多,所以说用了之后真香。

使用 Form 表单内的输入框,可以带来以下好处:

  1. 提高用户体验:自动校验可以帮助用户在输入时快速发现错误,并提供即时反馈,减少用户提交后的不必要等待和修正操作。自动保存可以防止用户数据丢失,让用户无需担心意外关闭或刷新页面而导致输入信息的丢失。

  2. 减少用户工作量:自动校验和自动显示下一步焦点可以帮助用户快速填写表单,减少冗余的操作。例如,当用户在一个字段中输入合法数据后,自动将焦点移动到下一个字段,使用户无需手动点击下一个输入框。

  3. 错误预防和纠正:自动校验可以在用户提交之前捕捉到错误,帮助用户避免提交无效或不完整的数据。同时,自动显示错误并刷新UI可以直接在界面上显示错误信息,引导用户进行必要的更正。

  4. 数据一致性和有效性:通过自动校验,可以确保用户输入的数据符合特定的规则和格式要求,从而提高数据的一致性和有效性。这有助于后续数据处理和分析的准确性。

  5. 提高工作效率:自动校验和自动保存功能可以减少用户的重复劳动,提高工作效率。用户不需要手动检查和保存数据,整个过程更加流畅和高效。

总之,Form 可以自动校验、自动保存、自动显示错误、自动显示下一步焦点等功能,可以提高用户体验、减少用户工作量、预防和纠正错误、保证数据一致性和有效性,以及提高工作效率。

如果你也没有用过 Form 那么我推荐你尝试一下哦。

Ok,那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出,如果有更多更好更方便的方式也欢迎大家评论区交流。

本文的代码已经全部贴出,部分没贴出的代码可以在前文中找到,也可以到我的 Flutter Demo 查看源码【传送门】

如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦!

Ok,这一期就此完结。

源文:【Flutter】Form表单,狗都不用到真香 & 如何自定义Form表单子控件

如有侵权请联系站点删除!

技术合作服务热线,欢迎来电咨询!