Masamune logo

Katana Form

Follow on Twitter Maintained with Melos


[YouTube](https://www.youtube.com/c/mathrunetchannel) | [Packages](https://pub.dev/publishers/mathru.net/packages) | [Twitter](https://twitter.com/mathru) | [LinkedIn](https://www.linkedin.com/in/mathrunet/)


Introduction

Form implementation is a very important part of the application.

It has become an indispensable interface that allows users to enter their information into the application.

Simplifying the implementation of Forms can be very helpful in increasing the speed and safety of application implementation.

Flutter provides FormField-type widgets such as Form and TextFormField.

However, it does not address data handling, and data acquisition and storage require implementation for each state management system.

Also, although it is possible to change the design with InputDecoration, I would like to simplify it and use it like ButtonStyle because there are many setting items.

For this reason, I have created the following package.

  • Enables input/output using a form by storing values for use in the form in the FormController and passing them to the FormController.
  • Unify design specifications by making FormStyle available to all form widgets. Enables easy unification of design.

It can be easily written as follows.


final form = FormController(<String, dynamic>{});

return Scaffold(
  appBar: AppBar(title: const Text("App Demo")),
  body: ListView(
    padding: const EdgeInsets.symmetric(vertical: 32),
    children: [
      const FormLabel("Name"),
      FormTextField(
        form: form,
        initialValue: form.value["name"],
        onSaved: (value) => {...form.value, "name": value},
      ),
      const FormLabel("Description"),
      FormTextField(
        form: form,
        minLines: 5,
        initialValue: form.value["description"],
        onSaved: (value) => {...form.value, "description": value},
      ),
      FormButton(
        "Submit",
        icon: Icon(Icons.check),
        onPressed: () {
          if (!form.validateAndSave()) {
            return;
          }
          print(form.value);
          // Save form.value as is.
        },
      ),
    ]
  )
);

This package can also be used with freezed to write code more safely.

Installation

Import the following packages

flutter pub add katana_form

Implementation

Create a Controller

First, define the FormController with initial values.

For new data creation, pass an empty object; for existing data, insert values read from the database.

This example is for a case where Map<String, dynamic> is used to handle data for a database.

// New data
final form = FormController(<String, dynamic>{});

// Existing data
final Map<String, dynamic> data = getRepositoryData();
final form = FormController(data);

This is maintained by a state management mechanism such as StatefulWidget.

Since FormController inherits from ChangeNotifier, it can be used in conjunction with riverpod's ChangeNotifierProvider, etc.

Form Implementation

Form widget installation is not required.

All you have to do is pass the FormController you created, and if you pass a FormController, you must also pass onSaved. (If you want to use only onChanged, you do not need to pass FormController.)

Pass the initial value to initialValue. When passing an initial value, pass the value obtained from FormController.value as is.

onSaved is passed the currently entered value as a callback, so be sure to return the changed FormController.value value as is.

FormTextField(
  form: form,
  initialValue: form.value["description"],
  onSaved: (value) => {...form.value, "description": value},
),

Form Validation and Storage

FormController.validateAndSave can be executed to validate and save the form.

Validation is performed first, and false is returned in case of failure.

If successful, true is returned and FormController.value is populated with the value changed by onSaved of each FormWidget.

Please update the database based on that value.

if (!form.validateAndSave()) {
	return;
}
print(form.value);
// Save form.value as is.

Sample code

The above sequence of events can be written in summary as follows

When destroying a form page, FormController should also be disposed of by disposing of it together with the form page.

class FormPage extends StatefulWidget {
  const FormPage({super.key});

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

class FormPageState extends State<FormPage> {
  final form = FormController(<String, dynamic>{});

  @override
  void dispose() {
    super.dispose();
    form.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("App Demo")),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 32),
        children: [
          const FormLabel("Name"),
          FormTextField(
            form: form,
            initialValue: form.value["name"],
            onSaved: (value) => {...form.value, "name": value},
          ),
          const FormLabel("Description"),
          FormTextField(
            form: form,
            minLines: 5,
            initialValue: form.value["description"],
            onSaved: (value) => {...form.value, "description": value},
          ),
          const SizedBox(height: 16),
          FormButton(
            "Submit",
            icon: Icon(Icons.add),
            onPressed: () {
              if (!form.validateAndSave()) {
                return;
              }
              print(form.value);
              // Save form.value as is.
            },
          ),
        ],
      ),
    );
  }
}

Freezed allows you to write code more safely.

@freezed
class FormValue with _$FormValue {
  const factory FormValue({
    String? name,
    String? description,
  }) = _FormValue;
}

class FormPage extends StatefulWidget {
  const FormPage({super.key});

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

class FormPageState extends State<FormPage> {
  final form = FormController(FormValue());

  @override
  void dispose() {
    super.dispose();
    form.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("App Demo")),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 32),
        children: [
          const FormLabel("Name"),
          FormTextField(
            form: form,
            initialValue: form.value.name,
            onSaved: (value) => form.value.copyWith(name: value),
          ),
          const FormLabel("Description"),
          FormTextField(
            form: form,
            minLines: 5,
            initialValue: form.value.description,
            onSaved: (value) => form.value.copyWith(description: value),
          ),
          const SizedBox(height: 16),
          FormButton(
            "Submit",
            icon: Icon(Icons.add),
            onPressed: () {
              if (!form.validateAndSave()) {
                return;
              }
              print(form.value);
              // Save form.value as is.
            },
          ),
        ],
      ),
    );
  }
}

Style Change

The style of each FormWidget can be changed together with FormStyle.

The default style is plain, but if you specify the following, the style will be changed to the Material design with a border.

FormTextField(
  form: form,
  initialValue: form.value["name"],
  onSaved: (value) => {...form.value, "name": value},
  style: FormStyle(
      border: OutlineInputBorder(),
      padding: const EdgeInsets.symmetric(horizontal: 16),
      contentPadding: const EdgeInsets.all(16)),
),

Type of FormWidget

Currently available FormWidget are

Adding as needed.

  • FormTextField
    • Field for entering text
  • FormDateTimeField
    • Field to select and enter the date and time in the Flutter dialog
  • FormDateField
    • Field to select the date (month and day) from a choice
  • FormNumField
    • Field to select a numerical value from a list of choices.
  • FormEnumField
    • Field to select from the Enum definition.
  • FormMapField
    • Field where you pass a Map and choose from its options.

The widgets that assist Form are as follows.

  • FormLabel
    • The label portion of the form is displayed separately. It also serves as a Divider.
  • FormButton
    • Used for confirm and cancel buttons for forms.
    • FormStyle is available.

Libraries

katana_form
Package to provide FormController to define the use of forms and FormStyle to unify the look and feel of forms.