Spark_Form

简介

spark form 是一个flutter平台的表单快速成型框架,其内部包括以下几个部分

  • 表单快速构建
  • 表单数据校验及数据管理
  • 自定义数据生成组件
  • 基于主题的自定义样式
  • 开放式组件及自定义接入组件
  • 焦点管理工具

基于上述功能,可以快速进行flutter的表单应用的开发,减少不必要的功能编写

表单数据处理

FormGroupManage

FormGroupManage是用于管理表单数据的生成及管理类,提供具名构造函数create

factory FormGroupManage.create(Map<String, ManageFieldItem> createField) =>
FormGroupManage._(createField);

接收的参数类型为Map<String, ManageFieldItem>其对应每一个字段所使用的组件类型的Option,并由 ManageFieldItem管理,你可以这样构建你的表单数据

final Map<String, ManageFieldItem> _fieldMap = {
  'test1': ManageFieldItem(data: ''),
  'test2': ManageFieldItem(
      data: SparkLabelTitleData.from('hahshdh', 'change value')),
  'test5': ManageFieldItem(data: '2'),
  'select': ManageFieldItem(
    data: List.generate(
      5,
          (index) => SparkTagGroupItem.from(
        name: 'hh$index a',
        data: '$index',
      ),
    ),
  ),
}

因为每一种组件所使用的数据类型不一致,所以在生成的时候,所对应的数据格式也不一致,以对应不同组件的数据生成,组件对应数据格式见每个组件的详细介绍

Tip: FormGroupManage必须生成在form构建之前 内部可以进行表单数据的获取

/// get json data
  /// exclude field[excludeField]is not include in json data
  Map getJson({List<String>? excludeField}) {
  ....
  }

只需要调用此方法,即可得到转换后的数据,类型为Map结构,其对应关系与传入的转换数据一致,用于表单数据的生成

SparkFormDataChangeImp

对于表单构建的数据,如果需要使用自定义数据,请继承实现,类包含两个字段,data为最后转换数据返回的数据,needSelected 为转化依据,只要当值为true时才会被转换

abstract class SparkFormDataChangeImp {
  SparkFormDataChangeImp(this.data);

  /// Serialized data
  /// [FormManage.getJson] use this filed to create json data
  dynamic data;

  /// only true can use by [FormManage.getJson]
  bool needSelected = false;
}

表单基础组件介绍

在基础组件中,应该遵循的封装原则是,组件可脱离form主体进行使用,即可单独使用

SparkForm

表单组件,是一个表单的顶层组件,接收参数为

const SparkForm({
    Key? key,
    required this.child,
    required this.manage,
    this.controller,
  }) : super(key: key);
  
  /// form controller
  final FormController? controller;

  /// form item builder
  final Widget child;

  /// form data manage
  final FormGroupManage manage;

在表单构建的时候,组件必须包裹在最外层,以提供SparkFormItem所需要的运行数据,组件接收一个SparkFormController作为内部控制器,可用于表单的校验和清除操作,见表单校验

SparkFormItem

表单子组件。其功能包括 label 布局生成,焦点管理,校验错误内容提示,等功能,接收参数为

const SparkFormItem({
    Key? key,
    required this.fieldName,
    required this.child,
    this.direction,
    this.labelBuilder,
    this.label,
    this.isRequired,
    this.rules,
    this.labelWidth,
    this.lablePadding,
    this.decoration,
    this.readOnly = false,
    this.padding,
    this.margin,
    this.labelStyle,
    this.crossCenter,
    this.errorStyle,
    this.errorContentPadding,
    this.labelSuffix,
  }) : super(key: key);

  /// form item bind field name
  final String fieldName;

  /// form label name
  final String? label;

  /// form label widget builder
  final WidgetThemeBuilder? labelBuilder;

  /// form widget builder
  final Widget child;

  /// form layout direction, set title layout in top or left
  final Direction? direction;

  /// form item is required
  final bool? isRequired;

  /// label width
  final double? labelWidth;

  /// setup label layout padding
  final EdgeInsets? lablePadding;

  /// form item decoration
  final FocusDecoration? decoration;

  /// value check rules
  final List<SparkValidator>? rules;

  /// set form item response focusNode
  /// if false, focus style is not't change
  /// [FocusPointerScope.shadow]
  final bool? readOnly;

  /// form item content padding
  final EdgeInsets? padding;

  /// form item content margin
  final EdgeInsets? margin;

  /// set label text style
  final TextStyle? labelStyle;

  /// cross use [CrossAxisAlignment.center]
  final bool? crossCenter;

  /// error tip message style
  final TextStyle? errorStyle;

  /// tip message content layout
  final EdgeInsets? errorContentPadding;

  /// label suffix, is left label text suffix
  /// not form item right suffix
  final Widget? labelSuffix;

组件提供丰富的自定义属性及布局方式,支持横行和纵向布局,使用direction来决定其布局方向,如同普通表单一样,在设置isRequired属性后,表单会显示*符号,用来表示其是否为必填项,但是它与内部校验无关,如果需要让该表单项不为空,请使用规则,同时,组件支持传入校验规则,只要在rules属性中加入不同的规则,即可进行不同的校验,见 表单校验, 比较特殊的是,组件内部使用了焦点吸附,表单项被选中时,会进行对应的焦点操作,对于样式的控制,有几个属性,包括readOnly decorationreadOnly设置为true时,焦点获取将不会更改样式,而样式的设置,可以使用decoration属性,见FocusDecoration,label设置时,如果是不需要使用label显示,不设置labellabelBuilder即可,其余属性均与布局有关,如crossCenter用于设置label于表单项的交叉轴对其方式,为true时将为居中对齐,否则使用顶部对齐,对于必须指定的项 fieldName,指每一项对应的表单数据

SparkLabelTitle

SparkLabelTitle为label组件,通常用于选择显示,下拉样式显示等,其样式与flutter的原生组件TitleLabel相似,其提供参数如下

const SparkLabelTitle({
    Key? key,
    required FormGroupItem manage,
    this.suffixIcon,
    this.showSuffix,
    this.suffixSize,
    this.maxLines,
    this.onTap,
    this.hitText,
    this.style,
    this.hitTextStyle,
    this.padding = EdgeInsets.zero,
    this.margin,
    this.width,
    this.selectDown = false,
    this.contentPadding,
  }) : super(key: key, manage: manage);

  /// custom suffix icon
  final Widget? suffixIcon;

  /// set show suffix icon
  final bool? showSuffix;

  /// set suffix icon size
  final Size? suffixSize;

  /// set text max line count
  final int? maxLines;

  /// label tap
  final SparkLabelTitleData? Function(SparkLabelTitleData data)? onTap;

  /// empty hit text
  final String? hitText;

  /// show text style
  final TextStyle? style;

  /// hit text style
  final TextStyle? hitTextStyle;

  /// set body padding
  final EdgeInsets padding;

  /// set body margin
  final EdgeInsets? margin;

  final double? width;

  /// choose suffix icon type
  final bool selectDown;

  /// set content and suffix padding
  final EdgeInsets? contentPadding;

提供包括文字提示、自定义文字内容显示,内部内容更新采用数据回传模式

final SparkLabelTitleData? Function(SparkLabelTitleData data)? onTap;

如果在onTap传入的函数中返回 SparkLabelTitleData类型的数据,将会把返回的数据更新到显示区域,并显示对应的内容,完成数据展示 组件包含另一个字段 selectDown 字段用于对尾部默认的箭头图标进行替换,如果值为true将显示向下的箭头 ,也可以设置showSuffix来设置是否显示尾部图标,或者suffixIcon来自定义图标

SparkSelect

组件为单选组件,用于选择情景,内部参数定义为

const SparkSelect({
    Key? key,
    required FormGroupItem manage,
    required this.options,
    this.itemBuilder,
    this.itemSpacing,
    this.itemSize,
    this.selectedDecoration,
    this.unSelectedDecoration,
    this.alignment,
  }) : super(key: key, manage: manage);

  /// option data
  final List<SparkSelectOption> options;

  /// custom item builder
  final ItemValueBuilder? itemBuilder;

  /// item space
  final double? itemSpacing;

  /// set up option size
  /// use BoxConstraints
  final Size? itemSize;

  /// set up select option style
  final SparkSelectDecoration? selectedDecoration;

  /// set up default option style
  final SparkSelectDecoration? unSelectedDecoration;

  /// option alignment
  final WrapAlignment? alignment;

对于组件来说,需要指定 options内容,用于显示选择的项,数据类型必须为SparkSelectOption,选中的数据为SparkSelectOption.value,会在表单系统中,自动与外层包裹的SparkFormItemfieldName字段所对应的属性对应,并更改其值,在进行自定义时itemBuilder将会把内部设置的样式及数据进行传递

/// spark select item options builder
/// [SparkSelect]
typedef ItemValueBuilder = Widget Function(BuildContext context, SparkOption value, SparkSelectDecoration decoration);

SparkTagGroup

标签选择器,器包含多种选择模式,如单选&添加、多选&添加、单选、多选,并可设置是否清除

const SparkTagGroup({
    Key? key,
    this.itemBuilder,
    required FormGroupItem manage,
    this.showClean = false,
    this.type = SparkTagGroupType.multiple,
    this.spacing = 10,
    this.runSpacing = 10,
    this.onAdd,
    this.addWidget,
    this.hitText,
    this.hitWidget,
    this.hitStyle,
    this.alignment,
    this.cleanIconBuilder,
  }) : super(key: key, manage: manage);

  /// custom item builder
  final TagGroupItemBuilder? itemBuilder;

  /// set up clean icon visible
  final bool showClean;

  /// group type
  final SparkTagGroupType type;

  /// see [Wrap]
  final double spacing;

  /// see [Wrap]
  final double runSpacing;

  /// group out side add function
  final SparkTagGroupItem? Function()? onAdd;

  /// costom add widget
  final Widget? addWidget;

  /// empty hit text
  final String? hitText;

  /// hit widget
  final Widget? hitWidget;

  /// hit text style
  final TextStyle? hitStyle;

  /// see [Wrap]
  final WrapAlignment? alignment;
  
  /// custom clean icon builder
  final TagGroupItemIconBuilder? cleanIconBuilder;

组件功能和Select类型,但是对于多标签选择和添加时,显得十分方便,已有的几种模式,已经能满足大部分的场景,只需要对不同的属性进行修改,即可达到对于的效果,特殊的地方在于,onAdd可给外部提供添加能力,或者,你也可以直接操作字段对应的数据,来达到添加的目的

SparkTextField

基于flutter本身TextField的封装,舍弃了一些原有的功能,对suffix prefix cleanble进行了封装,使用体验更加友好,其余使用与原生功能几乎一致

表单校验

SparkForm组件中,我们提供了一个SparkFormController,传入实例后,便可对表单进行校验操作

SparkFormController _controller = SparkFormController();
...
SparkForm(
    controller: _controller,
    manage: _manage,
    ...
)
...

SparkFormController

SparkFormController中,提供了多个校验方法

class SparkFormController {
 
  ValidateResult validateField(String fieldName, VerifyType type, {dynamic preValue}) {
    ...
  }

  /// validate all rules, get validate result
  /// [focusError] auto focus first error
  ValidateResult validate({bool? focusError}) {
    ...
  }

  /// reset all field data
  void resetAll() {
    _manage?.reset();
  }
}

如果你需要检测表单的正确性,只需要调用SparkFormController.validate方法即可,方法提供自动聚焦到第一项错误的功能,SparkFormController.resetAll则会将表单初始化为初始数据,对应的SparkFormController.validateField方法即是对单独的某一个字段进行验证

SparkValidator

对于表单的验证模块,即对应SparkFormItem中的rules字段,可以对每个属性的值进行验证,对于不同的属性值,验证规则也不同,spark_form内提供多个属性验证器

abstract class SparkValidator {
  SparkValidator({
    required this.errorMessage,
    required this.type,
    this.strategy,
  });

  /// verify error message
  String errorMessage;

  /// validate type
  VerifyType? type;

  /// validate strategy
  ///
  FormStrategy? strategy;

  /// validate rule
  ValidateResult validate(dynamic value, String fieldName, VerifyType? type);
}

每个规则可以对应不同的触发校验的方式,设置type即可,如果需要自己的校验器,继承于SparkValidator实现即可

enum VerifyType {
  /// verfiy value after changed
  change,

  /// verfiy value after blur
  blur,

  /// include change and blur
  all,
}

然而对于strategy字段,则是对规则的行为做出区分

enum FormStrategy {
  /// form change strategy, if valitate error, back to old value
  limit,

  /// just tip error message
  tip,
  
  /// Not to do anything
  noop,
}

规则设置为FormStrategy.limit时,值将会限制输入,无法变成不符合规则的值,而FormStrategy.tip则会触发表单的提示功能,最后一个属性则是不做任何事,用于区分校验策略 校验规则也各有不同,

SparkLengthValidator

提供一个字符串长度验证器

SparkLengthValidator.length(
    this.length, {
    required String errorMessage,
    required VerifyType? type,
    FormStrategy? strategy = FormStrategy.limit,
  }) : super(
          errorMessage: errorMessage,
          type: type,
          strategy: strategy,
        );

可以对属性值的长度进行验证(必须为字符串)

SparkFormatValidator

提供一个正则验证器

SparkFormatValidator.form(
    this.rule, {
    required String errorMessage,
    required VerifyType? type,
    FormStrategy? strategy = FormStrategy.limit,
  }) : super(
          errorMessage: errorMessage,
          type: type,
          strategy: strategy,
        );

可对属性值的格式进行验证(必须为字符串)

SparkNumberValidator

提供一个数字格式验证器,包含多种验证方式,包括字符格式正确性验证、大小范围验证、整数和小数长度验证,通过不同具名构造函数构建即可使用

后者的验证建立在数字格式的基础上进行,如果需要更加准确的提示,应该先使用number验证字符格式

 /// number only
  SparkNumberValidator.number({
    required String errorMessage,
    required VerifyType? type,
    FormStrategy? strategy = FormStrategy.limit,
  }) : super(
          errorMessage: errorMessage,
          type: type,
          strategy: strategy,
        );

  /// setup number range
  SparkNumberValidator.range({
    required this.max,
    required this.min,
    required String errorMessage,
    required VerifyType? type,
    FormStrategy? strategy = FormStrategy.limit,
  })  : assert(
          max > min,
          'SparkNumberValidator.range max must be max than min',
        ),
        super(
          errorMessage: errorMessage,
          type: type,
          strategy: strategy,
        );

  /// setup number integer part length and decimal part length
  SparkNumberValidator.length({
    required this.integerLength,
    required this.decimalLength,
    required String errorMessage,
    required VerifyType? type,
    FormStrategy? strategy = FormStrategy.limit,
  }) : super(
          errorMessage: errorMessage,
          type: type,
          strategy: strategy,
        );

对于校验值,也只能为字符串

SparkRequiredValidator

提供一个非空验证器

SparkRequiredValidator.required({
    required String errorMessage,
    required VerifyType? type,
    FormStrategy? strategy = FormStrategy.limit,
  }) : super(
          errorMessage: errorMessage,
          type: type,
          strategy: strategy,
        );

兼容已有数据结构

主题

对于spark_form来说。大部分情况下都可以通过组件上的自定义属性来更改不同的样式,但是有没有一种一劳永逸的方式来更改同表单内的样式呢?答案的存在的 在spark_form中存在一个组件SparkThemeData,其实现类似于flutter的ThemeData实现

class SparkThemeData extends InheritedWidget {
  /// form theme data manage provider
  const SparkThemeData({required Widget child, required this.theme, Key? key})
      : super(child: child, key: key);

  /// form theme
  final SparkTheme theme;

  /// get form theme data of context
  static SparkTheme? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<SparkThemeData>()?.theme;
  }

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return true;
  }
}

通过继承InheritedWidget实现一个数据共享中心,其包裹的组件都可以通过SparkThemeData.of获取其内部存储的theme,基于flutter提供的特性,我们实现了一套主题系统,你可以通过设置主题的方式,来初始化全局的表单样式,只需要将表单组件作为SparkThemeData的字组件即可,例如

SparkThemeData(child: SparkForm(
                controller: _controller,
                manage: _manage,
                child: ...
              ), theme: SparkTheme(),)

即可对一系列的样式进行初始化定义,方便统一的风格和主题的定制,SparkTheme目前支持的属性为

const SparkTheme({
    this.labelColor = _defaultFontColor,
    this.hitColor = _hitTextColor,
    this.labelFontSize = _defaultFormItemFontSize,
    this.labelFontWeight = FontWeight.w500,
    this.requiredColor = _errorOrRequiredColor,
    this.requiredPrefix = '*',
    this.formItemFontSize = _defaultFormItemFontSize,
    this.formItemColor = _defaultFormItemColor,
    this.formItemDisabledColor = _defaultFormItemDisabledColor,
    this.lableWidth = _defaultLabelWidth,
    this.lablePadding = EdgeInsets.zero,
    this.formItemDecoration = _defaultFormDecoration,
    this.formItemPadding = _defaultFormItemPadding,
    this.formItemMargin = _defaultFormItemMargin,
    this.selectItemSize = _defaultSelectItemSize,
    this.selectedItemDecoration = _defaultSelectedDecoration,
    this.unSelectedItemDecoration = _defaultUnSelectedDecoration,
    this.errorColor = _errorOrRequiredColor,
    this.tagItemDecoration = _defaultTagDecoration,
    this.tagSelectedItemDecoration = _defaultTagSelectedDecoration,
    this.tagStyle = _defaultTagItemStyle,
    this.tagSelectedStyle = _defaultSelectedItemStyle,
    this.tagMinWidth = _defaultTagWidth,
    this.tagAddDecoration = _defaultTagAddDecoration,
    this.tagContentPadding = _defaultTagContentPadding,
  });

主题和组件本身的样式并不冲突,如果组件内部设置了对应样式,将不会使用主题样式

目前主题功能还处于深度定制阶段,一些属性还未列为主题管理

焦点吸附组件

在spark_form中,提供了一个焦点吸附组件 FocusPointerScope,其实现是基于flutter的焦点管理体系的,在进行表单选择切换时,会自动进行焦点的获取,对于其内部实现为独立封装,可用于其他功能的实现,内部吸附的组件为一个Container可对焦点的响应做出各种行为,并暴露相应的事件

class FocusPointerScope extends StatefulWidget {
  const FocusPointerScope({
    Key? key,
    this.focusNode,
    required this.child,
    this.shadow = false,
    this.enabled,
    this.decoration,
    this.padding,
    this.margin,
    this.alignment,
    this.constraints,
    this.onTap,
    this.onFocus,
    this.onUnFocus,
    this.width,
    this.height,
  }) : super(key: key);

  final FocusNode? focusNode;

  final Widget child;

  /// only request focus,but not set focus style
  final bool? shadow;

  /// focus enable
  final bool? enabled;

  /// widget decoration
  final FocusDecoration? decoration;

  /// content padding
  final EdgeInsets? padding;

  /// content margin
  final EdgeInsets? margin;

  final AlignmentGeometry? alignment;

  final BoxConstraints? constraints;

  /// on widget tap
  final VoidCallback? onTap;

  /// on focus
  final VoidCallback? onFocus;

  /// on lose focus
  final VoidCallback? onUnFocus;

  final double? width;

  final double? height;

  @override
  State<StatefulWidget> createState() => _FocusPointerScopeBase();
}

其实现原理的:通过Listener进行事件监听,得到内部点击事件,然后进行焦点的获取

_handleTap() {
    ...
    FocusScope.of(context).requestFocus(_effectiveFocusNode);
  }

  @override
  void initState() {
    _effectiveFocusNode.addListener(_handleFocusChanged);
    // attach focusNode target widget
    _focusAttachment = _effectiveFocusNode.attach(context);
    super.initState();
  }

  @override
  void didUpdateWidget(covariant FocusPointerScope oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.focusNode != oldWidget.focusNode) {
      (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged);
      (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged);
      // replace focusNode, rebind context
      _focusAttachment?.detach();
      _focusAttachment = _effectiveFocusNode.attach(context);
    }
    _effectiveFocusNode.canRequestFocus = _canRequestFocus;
  }

  @override
  void dispose() {
    _effectiveFocusNode.removeListener(_handleFocusChanged);
    _focusNode?.dispose();
    _focusAttachment?.detach();
    super.dispose();
  }

由此可以得到一个焦点管理的实现,避免了焦点混乱

自定义表单组件

对于自定义表单组件,我们提供了两种封装思路

组件形式封装

基于组件的形式进行封装的话,组件应该对应以下类进行对应的继承实现

abstract class SparkFormStateLessWidget<D> extends StatelessWidget {
  const SparkFormStateLessWidget({Key? key, required this.manage}) : super(key: key);

  final FormGroupItem manage;

  D get fieldData => manage.notifier.data;

  void handleFormDataChange(BuildContext context, dynamic newValue) {
    FormDataChangeNotification(
      newValue: newValue,
    ).dispatch(context);
  }
}

abstract class SparkFormWidget extends StatefulWidget {
  const SparkFormWidget({Key? key, required this.manage}) : super(key: key);

  final FormGroupItem manage;
}

abstract class SparkFormWidgetBase<T extends SparkFormWidget, D> extends State<T> {

  D get fieldData => widget.manage.notifier.data;

  FormGroupItem get manage => widget.manage;

  void handleFormDataChange(dynamic newValue) {
    FormDataChangeNotification(
      newValue: newValue,
    ).dispatch(context);
  }

  @override
  void initState() {
    widget.manage.notifier.addListener(valueChangeNotify);
    super.initState();
  }

  void valueChangeNotify();

  @override
  void dispose(){
    widget.manage.notifier.removeListener(valueChangeNotify);
    super.dispose();
  }

}

分别对应有状态组件和无状态组件的继承实现,基类中存在方法handleFormDataChange,只需要将组件产生的新值通过此方法进行传递,form就可以处理到对应的项,然后在 valueChangeNotify进行回调执行,也就是校验完成后会执行此方法,可用于组件的刷新和其他处理

组件包裹

如果不需要使用到组件形式的实现,可以使用SparkWrapper来进行组件的包裹,但是需要注意的是,该形式无法触发表单的验证功能,仅仅提供表单内部值发生变化时的更新功能,实现方式是基于数据监听的方法实现

const SparkWrapper({
    Key? key,
    required this.builder,
    required FormGroupItem manage,
  }) : super(key: key, manage: manage);

只需要将你自己的组件包裹在其中,即可得到更新值和自定刷新的能力,缺点是不存在组件缓存,不建议消耗过大的组件使用此方法进行处理

Libraries

dashed_decoration
spark
spark_stack
spark_verification
spark_widget