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

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,这一期就此完结。

相关推荐
AiFlutter10 小时前
Flutter之Package教程
flutter
Mingyueyixi15 小时前
Flutter Spacer引发的The ParentDataWidget Expanded(flex: 1) 惨案
前端·flutter
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
老田低代码2 天前
Dart自从引入null check后写Flutter App总有一种难受的感觉
前端·flutter
AiFlutter2 天前
Flutter Web首次加载时添加动画
前端·flutter
ZemanZhang4 天前
Flutter启动无法运行热重载
flutter
AiFlutter4 天前
Flutter-底部选择弹窗(showModalBottomSheet)
flutter
帅次4 天前
Android Studio:驱动高效开发的全方位智能平台
android·ide·flutter·kotlin·gradle·android studio·android jetpack
程序者王大川5 天前
【前端】Flutter vs uni-app:性能对比分析
前端·flutter·uni-app·安卓·全栈·性能分析·原生
yang2952423615 天前
使用 Vue.js 将数据对象的值放入另一个数据对象中
前端·vue.js·flutter