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

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

前言

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

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

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

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

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

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

一、如何简单使用

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

  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,
    },
  };
           //电话号码
                    _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 表单内部的焦点变化到下一步。

  /// 执行手机号码的绑定
  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 表单之前的效果:

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

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

  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 的实现对象

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

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

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

/*
*  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;
});
};
}
}

使用:

          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),
),
);
}

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

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

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

  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 。

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


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;
},
),
),

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

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

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

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

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

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 中使用了,效果如下:

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

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

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

总结

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

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

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

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

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

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

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

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

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

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

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

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

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

Ok,这一期就此完结。

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

原文链接:https://juejin.cn/post/7355387036579053577 作者:Newki

(0)
上一篇 2024年4月9日 上午10:59
下一篇 2024年4月9日 上午11:10

相关推荐

发表回复

登录后才能评论