Flutter表单生成插件cumulations_form_generators的使用

Flutter表单生成插件cumulations_form_generators的使用

Cumulations Form Generator

Cumulations Form Generator 用于为数据类生成表单。它使用注解来生成表单。该包还包括一个表单生成器,基于 cumulation_form_annotations 包生成表单。

注解列表及其用途:

  • @FormGenerate: 用于为数据类生成表单。只需在类上添加此注解即可创建表单。它接受以下参数:

    • name: 表单的标题。
    • maxWidth: 如果你希望定义表单的宽度。
  • @Field: 用于声明类中的变量作为表单字段。要使用它,只需在变量上添加此注解。你可以通过以下参数指定输入字段的类型:

    • label: 字段的标签。
    • isRequired: 字段是否必需,默认为 false
    • FieldType: 字段的类型。
      • FieldType.TextInput
      • FieldType.TextArea
      • FieldType.CheckBox
      • FieldType.SingleSelect
      • FieldType.MultiSelect
      • FieldType.DatePicker
  • @Options: 此注解允许你为单选和多选字段提供预定义选项。

  • @DynamicOptions: 使用此注解可以动态提供选项。为了实现这一点,你需要在 CategoryMasterDataMapper 类下创建一个 static getDropdownItemByGroupId 函数,在运行时提供选项。

安装包

在你的 pubspec.yaml 文件中添加以下依赖项:

dependencies:
  cumulation_form_annotations: ^1.0.0

dev_dependencies:
  build_runner:
  cumulations_form_generators: ^1.0.0

或者使用以下命令:

dart pub add dev:cumulations_form_generators
dart pub add cumulation_form_annotations

然后运行 flutter pub getdart pub get

使用

创建一个数据模型文件,格式为 <FileName>_form.dart,并在类上添加 [@FormGenerate](/user/FormGenerate) 注解,并在字段上添加 [@Field](/user/Field)[@Options](/user/Options)[@DynamicOptions](/user/DynamicOptions)(如果需要)。

注意: 文件名应包含 _form

import 'package:flutter/material.dart'; // 导入这个包到你的数据模型类中
import 'package:cumulations_form_annotations/cumulations_form_annotations.dart';
import 'package:project_package/SearchableMultiSelect.dart'; // 如果你使用的是 FieldType.MultiSelect
import 'package:project_package/date_picker_textfield.dart'; // 如果你使用的是 FieldType.DatePicker

part 'FileName_form.g.dart'; // 在文件顶部添加这一行

[@FormGenerate](/user/FormGenerate)("News letter subscription")
class FileName {
  // 对于表单中的文本字段
  // String 是首选的 TextInput 和 TextArea。但是,你可以使用任何类型,但在提交表单时必须将其从 String 转换为你所需的类型
  [@Field](/user/Field)("First Name", FieldType.TextInput, requiredFiled: true)
  String? fName;

  [@Field](/user/Field)("Last Name", FieldType.TextInput)
  String? lName;

  [@Field](/user/Field)("Email", FieldType.TextInput)
  String? email;

  // 对于 SingleSelect 或 MultiSelect,除了 [@Field](/user/Field) 还需要使用 [@Options](/user/Options) 或 [@DynamicOptions](/user/DynamicOptions)
  // 如果你使用 [@Options](/user/Options),必须传递 `Map<T, String>` 作为选项。其中 T 是键的类型,String 是值的类型,在这种情况下我们使用 int 作为键的类型
  // 请保持变量的数据类型为 T,在我们的例子中 T 是 int
  // 如果你使用 [@DynamicOptions](/user/DynamicOptions),必须传递要在下拉菜单中显示的选项数量
  // 在这种情况下,你必须在 CategoryMasterDataMapper 类下实现静态的 getDropdownItemByGroupId 函数
  [@Field](/user/Field)("Gender", FieldType.SingleSelect, requiredFiled: true)
  [@Options](/user/Options)(options: {1: "Male", 2: "Female"})
  int? gender;

  // 如果你想在表单中使用 MultiSelect,需要在项目中添加 SearchableMultiselect 的代码并导入到数据模型类中
  [@Field](/user/Field)("Topic interested In", FieldType.MultiSelect, requiredFiled: true)
  [@DynamicOptions](/user/DynamicOptions)(2)
  List<int>? multiSelect;

  // 对于 DatePicker,需要在项目中使用 DatePickerTextField 并导入到数据模型类中
  [@Field](/user/Field)("Date of Birth", FieldType.DatePicker, requiredFiled: true)
  DateTime? date;

  // 对于 CheckBox,需要使用 bool? 作为数据类型
  [@Field](/user/Field)("Do you want to opt in for promotions", FieldType.CheckBox)
  bool? opt_for_promotion;

  // 对于表单中的文本区域
  [@Field](/user/Field)("Address", FieldType.TextArea, requiredFiled: true)
  String? address;

  Subscriber({this.fName,
    this.lName,
    this.email,
    this.gender,
    this.opt_for_promotion,
    this.multiSelect,
    this.date,
    this.address});
}

如果你使用了 [@DynamicOptions](/user/DynamicOptions),需要在 CategoryMasterDataMapper 类下实现 static getDropdownItemByGroupId 函数。

class CategoryMasterDataMapper {
  static List<Topics> getDropdownItemByGroupId(int formId) {
    switch (formId) {
      case 2:
        return [
          Topics(1, "Sports"),
          // 请保持 T 数据类型与字段定义的数据类型相同
          Topics(2, "Politics"),
          Topics(3, "International")
        ];
      // 根据 formId 返回选项列表
      default:
        return [];
    }
  }
}

class Topics<T> {
  T id;
  String name;

  Topics(this.id, this.name);
}

对于多选字段,你需要在项目中添加以下代码:

import 'package:aligned_dialog/aligned_dialog.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';

class SearchableMultiselect extends StatefulWidget {
  final List<OptionType> items;
  final List<OptionType> selectedValues;
  final Function(List<dynamic>) onSelect;

  const SearchableMultiselect({Key? key,
    required this.items,
    required this.selectedValues,
    required this.onSelect})
      : super(key: key);

  @override
  State<SearchableMultiselect> createState() => _SearchableMultiselectState();
}

class _SearchableMultiselectState extends State<SearchableMultiselect> {
  final GlobalKey _widgetKey = GlobalKey();

  late final TextEditingController _textEditingController =
  TextEditingController();

  final FocusNode _focusNode1 = FocusNode();

  @override
  void initState() {
    super.initState();
    _textEditingController.text = widget.selectedValues.isNotEmpty
        ? "${widget.selectedValues.length} Selected"
        : "";
    print("new render");
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      style: TextStyle(fontSize: 12),
      key: _widgetKey,
      focusNode: _focusNode1,
      keyboardType: TextInputType.none,
      decoration: const InputDecoration(
          hintText: "Select", suffixIcon: Icon(Icons.arrow_drop_down)),
      controller: _textEditingController,
      onTap: () {
        showAlignedDialog(
            context: context,
            builder: _localDialogBuilder,
            followerAnchor: Alignment.topCenter,
            targetAnchor: Alignment.bottomCenter,
            avoidOverflow: true,
            barrierColor: Colors.transparent);
      },
    );
  }

  WidgetBuilder get _localDialogBuilder {
    final RenderBox renderBox =
    _widgetKey.currentContext?.findRenderObject() as RenderBox;
    return (BuildContext context) {
      return Container(
        constraints:
        BoxConstraints(maxWidth: renderBox.size.width, maxHeight: 250),
        child: Material(
            elevation: 4.0,
            child: _OptionList(
              items: widget.items,
              selectedValues: widget.selectedValues,
              onSelect: (value, data) {
                widget.onSelect(value);
                setState(() {
                  _textEditingController.text = data;
                });
              },
            )),
      );
    };
  }
}

class _OptionList extends StatefulWidget {
  final List<OptionType> items;
  final List<OptionType> selectedValues;
  final Function(List<dynamic>, String) onSelect;

  const _OptionList({Key? key,
    required this.items,
    required this.selectedValues,
    required this.onSelect})
      : super(key: key);

  @override
  State<_OptionList> createState() => _OptionListState();
}

class _OptionListState extends State<_OptionList> {
  late final TextEditingController _textFieldController =
  TextEditingController();
  final FocusNode _focusNode2 = FocusNode();

  List<OptionType> _searchResult = [];
  List<OptionType> _selectedList = [];

  @override
  void initState() {
    super.initState();
    _selectedList = widget.selectedValues;
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          child: Padding(
            padding: const EdgeInsets.all(0.2),
            child: Card(
              child: ListTile(
                leading: const Icon(Icons.search),
                title: TextField(
                  style: TextStyle(fontSize: 12),
                  focusNode: _focusNode2,
                  controller: _textFieldController,
                  decoration: const InputDecoration(
                      hintText: 'Search', border: InputBorder.none),
                  onChanged: _onSearchTextChanged,
                ),
              ),
            ),
          ),
        ),
        Container(
          constraints: BoxConstraints(maxHeight: 180),
          child: _searchResult.length != 0 ||
              _textFieldController.text.isNotEmpty
              ? SingleChildScrollView(
            child: Column(
              children: [
                ListView.builder(
                  itemCount: _searchResult.length,
                  shrinkWrap: true,
                  physics: NeverScrollableScrollPhysics(),
                  itemBuilder: (context1, i) {
                    return GestureDetector(
                      onTap: () async {
                        if (_selectedList.contains(_searchResult[i])) {
                          _selectedList.remove(_searchResult[i]);
                        } else {
                          _selectedList.add(_searchResult[i]);
                        }
                        var selectedValue = <dynamic>[];
                        _selectedList.forEach((element) {
                          selectedValue.add(element.value);
                        });
                        widget.onSelect(
                            selectedValue,
                            _selectedList.isNotEmpty
                                ? "${_selectedList.length} Selected"
                                : "");
                        setState(() {});
                      },
                      child: Card(
                        color: _selectedList.contains(_searchResult[i])
                            ? Colors.green
                            : Colors.white,
                        margin: const EdgeInsets.all(0.0),
                        child: ListTile(
                          title: Text(
                              style: TextStyle(
                                fontSize: 12,
                              ),
                              _searchResult[i].label),
                        ),
                      ),
                    );
                  },
                ),
              ],
            ),
          )
              : SingleChildScrollView(
            child: Column(
              children: [
                ListView.builder(
                  itemCount: widget.items.length,
                  shrinkWrap: true,
                  physics: NeverScrollableScrollPhysics(),
                  itemBuilder: (context, index) {
                    return GestureDetector(
                      onTap: () {
                        if (_selectedList.contains(widget.items[index])) {
                          _selectedList.remove(widget.items[index]);
                        } else {
                          _selectedList.add(widget.items[index]);
                        }

                        var selectedValue = <dynamic>[];
                        _selectedList.forEach((element) {
                          selectedValue.add(element.value);
                        });
                        widget.onSelect(
                            selectedValue,
                            _selectedList.isNotEmpty
                                ? "${_selectedList.length} Selected"
                                : "");
                        setState(() {});
                      },
                      child: Card(
                        color: _selectedList.contains(widget.items[index])
                            ? Colors.green
                            : Colors.white,
                        margin: const EdgeInsets.all(0.0),
                        child: ListTile(
                          title: Text(
                              style: TextStyle(
                                fontSize: 12,
                              ),
                              widget.items[index].label),
                        ),
                      ),
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

  _onSearchTextChanged(String text) async {
    print("text: $text");
    _searchResult.clear();
    if (text.isEmpty) {
      setState(() {});
      return;
    }
    RegExp pattern = RegExp(text, caseSensitive: false);
    widget.items.forEach((value) {
      if (value.label.contains(pattern)) {
        _searchResult.add(value);
      }
    });

    setState(() {});
  }
}

class _AlwaysDisabledFocusNode extends FocusNode {
  @override
  bool get hasFocus => false;
}

class OptionType extends Equatable {
  final dynamic value;
  final String label;

  OptionType({required this.value, required this.label});

  @override
  List<Object> get props => [value];
}

对于日期选择字段,你需要在项目中添加以下代码:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class TestPickerWidget extends StatefulWidget {
  final DateTime? selectedDate;
  final ValueSetter<DateTime> onSelectDate;
  final DateTime? initialDate;
  final DateTime? firstData;
  final DateTime? lastDate;

  const TestPickerWidget({
    Key? key,
    this.selectedDate,
    this.initialDate,
    this.firstData,
    this.lastDate,
    required this.onSelectDate,
  }) : super(key: key);

  @override
  _TestPickerWidgetState createState() => _TestPickerWidgetState();
}

class _TestPickerWidgetState extends State<TestPickerWidget> {
  late DateTime _selectedDate;
  late final TextEditingController _textEditingController =
  TextEditingController(
      text: (widget.selectedDate != null)
          ? DateFormat("MM/dd/yyy").format(widget.selectedDate!)
          : "");

  @override
  Widget build(BuildContext context) {
    return Center(
        child: TextFormField(
          focusNode: AlwaysDisabledFocusNode(),
          controller: _textEditingController,
          onTap: () {
            _selectDate(context);
          },
          decoration: const InputDecoration(
              suffixIcon: Icon(Icons.calendar_month), hintText: "MM/DD/YYYY"),
        ));
  }

  /// 显示日期选择器并返回所选日期
  _selectDate(BuildContext context) async {
    DateTime? newSelectedDate = await showDatePicker(
        context: context,
        initialDate:
        widget.initialDate ?? widget.selectedDate ?? DateTime.now(),
        firstDate: widget.firstData ??
            DateTime.fromMillisecondsSinceEpoch(DateTime
                .now()
                .year - 10),
        lastDate: widget.lastDate ?? DateTime(DateTime
            .now()
            .year + 10),
        currentDate: DateTime.now(),
        builder: (context, child) {
          return Theme(
            data: ThemeData.light().copyWith(
                colorScheme: ColorScheme.light(
                  primary: Color(0xFF2E2E48),
                  onPrimary: Colors.white,
                ),
                // 这里我更改了 overline 的样式
                textTheme: TextTheme(
                    overline:
                    TextStyle(color: Color(0xFF2E2E48), fontSize: 12)),
                primaryTextTheme: TextTheme(
                    overline: TextStyle(color: Color(0xFF2E2E48), fontSize: 12))
              // dialogBackgroundColor: Colors.white,
            ),
            child: child!,
          );
        });

    if (newSelectedDate != null) {
      _selectedDate = newSelectedDate;
      _textEditingController
        ..text = DateFormat("MM/dd/yyyy").format(_selectedDate)
        ..selection = TextSelection.fromPosition(TextPosition(
            offset: _textEditingController.text.length,
            affinity: TextAffinity.upstream));
      widget.onSelectDate(_selectedDate);
    }
  }
}

class AlwaysDisabledFocusNode extends FocusNode {
  @override
  bool get hasFocus => false;
}

生成表单的命令

flutter pub run build_runner build

一旦运行上述命令,将生成一个新的文件,名为 <FileName>_form.g.dart,位于数据模型文件所在的同一目录中。

你可以通过调用 getForm 方法使用数据模型对象获取表单。

@override
Widget build(BuildContext context) {
  return Scaffold(
      body: SingleChildScrollView(
          child: Column(
              children: [
                dataModelObject.getForm()
              ]
          )));
}

如果你想从表单获取数据,可以调用 getFormData 方法。这将返回一个映射。

dataModelObject.getFormData()

更多关于Flutter表单生成插件cumulations_form_generators的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter表单生成插件cumulations_form_generators的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何使用 cumulations_form_generators 插件在 Flutter 中生成表单的示例代码。这个插件可以大大简化表单字段的生成和管理。

首先,确保你已经在 pubspec.yaml 文件中添加了 cumulations_form_generators 依赖:

dependencies:
  flutter:
    sdk: flutter
  cumulations_form_generators: ^最新版本号  # 请替换为实际的最新版本号

然后,运行 flutter pub get 来获取依赖。

接下来,你可以使用 CumulationsFormBuilders 来生成表单字段。以下是一个完整的示例代码:

import 'package:flutter/material.dart';
import 'package:cumulations_form_generators/cumulations_form_generators.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _formKey = GlobalKey<FormState>();

  // 用于存储表单数据
  final Map<String, dynamic> _formData = {
    'name': '',
    'email': '',
    'age': '',
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Form Example'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              // 使用 CumulationsFormBuilders 生成文本表单字段
              CumulationsFormBuilders.textFormField(
                'name',
                labelText: 'Name',
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your name';
                  }
                  return null;
                },
                onSaved: (value) {
                  _formData['name'] = value;
                },
              ),

              CumulationsFormBuilders.textFormField(
                'email',
                labelText: 'Email',
                keyboardType: TextInputType.emailAddress,
                validator: (value) {
                  if (value == null || value.isEmpty || !value.contains('@')) {
                    return 'Please enter a valid email address';
                  }
                  return null;
                },
                onSaved: (value) {
                  _formData['email'] = value;
                },
              ),

              CumulationsFormBuilders.numberFormField(
                'age',
                labelText: 'Age',
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your age';
                  }
                  if (int.tryParse(value) == null || int.parse(value) <= 0) {
                    return 'Please enter a valid age';
                  }
                  return null;
                },
                onSaved: (value) {
                  _formData['age'] = value;
                },
              ),

              SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                    _formKey.currentState!.save();
                    // 在这里处理表单数据,例如提交到服务器
                    print('Form Data: $_formData');
                  }
                },
                child: Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

在这个示例中,我们使用了 CumulationsFormBuilders 来生成三个表单字段:文本字段(用于名称和电子邮件),以及数字字段(用于年龄)。每个字段都包含了标签文本、验证器和保存回调。当用户点击提交按钮时,表单数据会被验证并保存,然后打印到控制台。

请确保你已经安装了最新版本的 cumulations_form_generators 插件,并根据需要调整字段和验证逻辑。

回到顶部