Flutter 動態表單Dynamic FormField架構設計

架構圖

Dynamic FormField
用了幾年前設計的Table架構圖,是kotlin版本的動態表單框架,也一樣適用於如今的設計,此次從設計到實現,其實經歷了不少,前期看官方文檔FormField的用法,還有一些現有的動態表單框架,一開始選擇用通常的StatefulWidget實現,但作了幾個發現一個問題,各個Widget的狀態管理,數據的變化,或者說統一的驗證提交等操做,須要太多的實現,將來簡化實現,最終仍是選擇用FormField,拓展它的子類來更好的管理表單。請仔細看圖,我來解釋下。
整個架構圖分兩個部分html

  • 第一部分展現的是此次框架的主角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,這樣你就能從源頭瞭解到該框架。git

實現一個FormField子類

建立一個FieldTest類github

import 'package:flutter/material.dart';

class FieldTest extends FormField{

}

沒有任何提示,也不用實現什麼,那怎麼辦?這個時候就須要點進去看下FormField源碼,經過對源碼的分析,咱們有一個清晰的認識json

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

一個沒什麼交互的簡版實現了,沒有交互那豈不是扯的嗎,表單輸入怎麼也得有個輸入框吧,哈哈,好咱們接着實現。app

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函數。
下面來實現一個校驗規則ide

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

相關文章
相關標籤/搜索