Flutter 动态表单Dynamic FormField架构设计

架构图

Dynamic FormField

用了几年前设计的Table架构图,是kotlin版本的动态表单框架,也同样适用于现在的设计,这次从设计到实现,其实经历了很多,前期看官方文档FormField的用法,还有一些现有的动态表单框架,一开始选择用一般的StatefulWidget实现,但做了几个发现一个问题,各个Widget的状态管理,数据的变化,或者说统一的验证提交等操作,需要太多的实现,未来简化实现,最终还是选择用FormField,拓展它的子类来更好的管理表单。请仔细看图,我来解释下。
整个架构图分两个部分

  • 第一部分展示的是这次框架的主角FormBuilder在Page页面中的位置,以及基本的属性定义
    formController 是对表单统一管理的抽象,可以对表单做验证validator,重置所有表单状态reset,保存save等,未来根据需求再拓展
    showSubmitButton 显示提交按钮,有自己的提交按钮可以设置false隐藏
    onSubmit 数据校验后的callBack回调,返回数据验证结果
    mapperFactory 这个是FormField动态扩展的关键,通过它就是让其他人动态实现一个自己的FormField,用来满足特殊的业务需求。
    itemList 这个是mapperFactory将业务数据集合FormItem转换成对应的Widget集合,最终显示的当前页面。
  • 第二部分展示了一个动态表单的业务流程,从服务器下发数据,到映射成对应的FormItemList,再由MapperFactory转换成对应的Widget,最终交给FormBuilder,再由FormBuilder生成一个Form,通过一个ListView动态的展示所有的FormField,并通过FieldValidator的抽象实现来做最终的数据校验,这是大致的流程。
    希望两种表达,能让你对整个框架有一个清晰的认识,接下来我们就聊一下,如何拓展一个FormField,这样你就能从源头了解到该框架。

实现一个FormField子类

创建一个FieldTest类

import 'package:flutter/material.dart';

class FieldTest extends FormField{

}

没有任何提示,也不用实现什么,那怎么办?这个时候就需要点进去看下FormField源码,通过对源码的分析,我们有一个清晰的认识

import 'framework.dart';
import 'navigator.dart';
import 'will_pop_scope.dart';

class Form extends StatefulWidget 

class FormState extends State<Form> 

class _FormScope extends InheritedWidget 

typedef FormFieldValidator<T> = String Function(T value);

typedef FormFieldSetter<T> = void Function(T newValue);

typedef FormFieldBuilder<T> = Widget Function(FormFieldState<T> field);

class FormField<T> extends StatefulWidget 

class FormFieldState<T> extends State<FormField<T>> 

一共涉及到五个类,三个函数,Form 其实这个类你可以理解为一个ListView的角色,它对FormField负责,管理等。_FormScope是InheritedWidget的子类,用来做数据共享的,从上往下的共享数据,Form的child最终被包装到了_FormScope中。三个函数分别实现了数据校验FormFieldValidator,数据更新FormFieldSetter,构建child widget的FormFieldBuilder,我们拓展的构建的child就是通过FormFieldBuilder函数。我们再仔细看下FormField类,源码如下

class FormField<T> extends StatefulWidget {

  /// [builder] 不能为空.
  const FormField({
    Key key,
    @required this.builder,
    this.onSaved,
    this.validator,
    this.initialValue,
    this.autovalidate = false,
    this.enabled = true,
  }) : assert(builder != null),
       super(key: key);

  /// 可选函数,数据保存时回调
  /// [FormState.save].
  final FormFieldSetter<T> onSaved;

  /// 可选函数,用于数据的校验
  final FormFieldValidator<T> validator;

  /// 构建子widget
  final FormFieldBuilder<T> builder;

  /// 可选参数,默认值
  final T initialValue;

  ///是否自动触发校验
  final bool autovalidate;

  /// 控制是否能输入,默认true
  final bool enabled;

  @override
  FormFieldState<T> createState() => FormFieldState<T>();
}

///[FormField]的当前状态。传递给[FormFieldBuilder]方法,用于构造表单字段的小部件。
class FormFieldState<T> extends State<FormField<T>> {
  ///表单数据T
  T _value;
 /// 要显示的错误信息
  String _errorText;

  /// 当前表单的值
  T get value => _value;

  /// 获取当前错误信息
  String get errorText => _errorText;

  /// 判断是否有错误
  bool get hasError => _errorText != null;

  /// 验证数据是否有效
  bool get isValid => widget.validator?.call(_value) == null;

  /// 保存数据,回调onSaved函数,并把数据传递给你
  void save() {
    if (widget.onSaved != null)
      widget.onSaved(value);
  }

  /// 重置数据
  void reset() {
    setState(() {
      _value = widget.initialValue;
      _errorText = null;
    });
  }

  /// 校验数据,主动验证,并刷新UI
  bool validate() {
    setState(() {
      _validate();
    });
    return !hasError;
  }
  /// 刷新错误信息
  void _validate() {
    if (widget.validator != null)
      _errorText = widget.validator(_value);
  }

  /// 更新当前页面数据
  void didChange(T value) {
    setState(() {
      _value = value;
    });
    ///当窗体字段更改时调用。通知所有表单字段要重新生成,如果有连动的效果,就显现出来了。
    Form.of(context)?._fieldDidChange();
  }

  /// 此方法只能由需要更新的子类调用,说了很长意思是,不然你通过它更新数据,应该调用didChange来设置数据。
  @protected
  void setValue(T value) {
    _value = value;
  }

  @override
  void initState() {
    super.initState();
    _value = widget.initialValue;
  }

  @override
  void deactivate() {
    /// 这里发现了当前Form注销掉了当前state的
    Form.of(context)?._unregister(this);
    super.deactivate();
  }

  @override
  Widget build(BuildContext context) {
    // Only autovalidate if the widget is also enabled
    if (widget.autovalidate && widget.enabled)
      _validate();
    /// 注册引用
    Form.of(context)?._register(this);
    return widget.builder(this);
  }
}

请先看注释,通过FormField构造,我们了解到,它最主要就是抽象了对数据T的初始化,校验,通知其他人我更新了,重置,保存等一系列的操作,我们应该关心的就是,如何validate,如何didChange,然后如何save数据对吧,这样我们就可以自己实现了,那么我们接下来就要开始实现了

class FieldTest extends FormField {
  FieldTest(
      {Key key,
      String initialValue,
      FormFieldSetter<String> onSaved,
      FormFieldValidator<String> validator,})
      : super(
            key: key,
            initialValue: initialValue,
            onSaved: onSaved,
            validator: validator,
            builder: (state) {
              return Container(
                child: Text(initialValue),
              );
            });
}

一个没什么交互的简版实现了,没有交互那岂不是扯的吗,表单输入怎么也得有个输入框吧,哈哈,好我们接着实现。

class FieldTest extends FormField {
  final ValueChanged<String> onChanged;
  FieldTest(
      {Key key,
      String initialValue,
      FormFieldSetter<String> onSaved,
      FormFieldValidator<String> validator,
      this.onChanged})
      : super(
            key: key,
            initialValue: initialValue,
            onSaved: onSaved,
            validator: validator,
            builder: (state) {
              return Container(
                child: Column(
                  children: <Widget>[
                    Text(initialValue),
                    TextField(
                      onChanged: onChanged,
                    )
                  ],
                ),
              );
            });
}

这次加了一个onChanged,用来监听输入框的更新,然后给父类的value赋值,然后触发相应的操作,比如校验什么的。

 builder: (state) {

              void _handleOnChanged(String _value) {
                if (state.value != _value) {
                  state.didChange(_value);
                  if (onChanged != null) {
                    onChanged(_value);
                  }
                }
              }
              
              return Container(
                child: Column(
                  children: <Widget>[
                    Text(initialValue),
                    TextField(
                      onChanged: _handleOnChanged,
                    )
                  ],
                ),
              );
            }

在builder 函数中添加一个_handleOnChanged函数,用来接管输入框的值,并更新到value上。这里解释下为什么,看FormFieldState源码应该知道,所有校验,重置,保存的操作都是T _value;这个变量,所以在输入框的实现中,我们肯定是对输入的内容做校验或者其他操作,所以这里要去更加输入的内容来更新_value的值,而更新的办法就是通过state.didChange函数。
下面来实现一个校验规则

String isValidator(value) {
  if (value == null) return "is null";
  if (value.isEmpty) return "is Empty";
  if (value.length > 6) return null;
  ///返回null说明校验通过
  return "value <= 6";
}

然后传递给它

FieldTest(
      {Key key,
      String initialValue,
      FormFieldSetter<String> onSaved,
      FormFieldValidator<String> validator = isValidator, /// 这里
      this.onChanged})

这样我们的校验规则有了,我们可以试下ok不,把组件加载到Page内

 Form(
                key: _formKey,
                child: FieldTest(
                  onChanged: (value) {
                    print("FieldTest onChanged value$value");
                  },
                ),
              ),
              RaisedButton(
                onPressed: (){
                  if(_formKey.currentState.validate()){
                    print("FieldTest验证通过");
                  }else{
                    print("FieldTest验证失败");
                  }
                },
                child: Text("校验FieldTest"),
              ),

嵌套在Form内,然后通过_formKey可以做管理,如validate()校验数据



输入23



控制台对应输出,校验一下,点击按钮打印


当我们再次输入超过校验规则的数据时,验证通过,其实就是这么的简单就实现了。在理解的基础上可以拓展更多的操作,比如显示错误信息,数据格式化,数据自动映射等等操作。接下来我们要做的就是对整个框架的拓展,拓展更多的FormField。

总结

动态表单在实际的业务开发中,有相当多的业务场景,特别是针对ToB的业务,表单的提交,校验就更别说了,越来越多,越来越复杂,如果说能有一个合适框架来减少那些本来就很简单但充斥着大量重复的操作,同样也可以解决那些负责的操作,何乐而不为呢。
项目源码:https://github.com/ibaozi-cn/flutter_dynamic_form

感谢大佬们的项目和文章

sirily11/json-textfrom
codegrue/card_settings
https://medium.com/flutter-community/flutter-how-to-validate-fields-dynamically-created-40cafca5c3cb
https://stackoverflow.com/questions/55463981/whats-the-best-way-to-dynamically-load-form-fields-in-flutter
https://book.flutterchina.club/chapter7/inherited_widget.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • Django教程--Form表单 前面我们已经了解如何在django中使用GET、POST传递数据,但是我们并没有...
    iffly阅读 892评论 0 4
  • 表单介绍 HTML表单负责接收用户的输入,对输入进行合法格式判断,并将数据发送到服务器。一个HTML表单必须指定两...
    Py_Bird阅读 3,632评论 0 1
  • 自离开你后 我一个人走了很久 看过树下落英缤纷 小桥细水长流 有时想起曾情深懵懂 曾执一字一句假设的离开你后 看看...
    巴克比克阅读 178评论 0 0
  • 打开朋友圈,经常看到会有这样的说说:又是一个美好的周末!么么哒!附上一个自拍照,或者美食图,阳光而又灿烂!周...
    鸣歌2018阅读 1,510评论 3 3
  • 龙应台在她的《目送》中曾这样描述:“所谓父女母子一场,只不过意味着,你和他的缘分就是今生今世不断地在目送他的背影渐...
    背相机的考拉阅读 567评论 0 0