2022-11-13

[Flutter] Katana Form

Hello. My name is Masaru Hirose.

I do not know the best practices for implementing Form.

I were puzzled by how to implement efficient and easy forms when creating new data or updating data.

Therefore, I made

Package to define form implementation in a simplified manner

In addition to handling form data, I can also handle the design together.

I’ve put together some instructions on how to use it, so if you’re interested, go ahead and give it a try!

katana_form

Package to provide FormController to define the use of forms and FormStyle to unify the look and feel of forms.
https://pub.devhttps://pub.dev
title

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: () {
          final value = form.validate(); // Validate and get form values
          if (value == null) {
            return;
          }
          print(value);
          // Save 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

You can validate and save a form by executing FormController.validate.

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

If it succeeds, it returns the value changed by onSaved of each Form widget.

Update the database based on that value.

final value = form.validate(); // Validate and get form values
if (value == null) {
  return;
}
print(value);
// Save 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: () {
		          final value = form.validate(); // Validate and get form values
		          if (value == null) {
		            return;
		          }
		          print(value);
		          // Save 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: () {
		          final value = form.validate(); // Validate and get form values
		          if (value == null) {
		            return;
		          }
		          print(value);
		          // Save 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.

Conclusion

I made it for my own use, but if you think it fits your implementation philosophy, by all means, use it!

Also, I releasing the source here, so issues and PullRequests are welcome!

If you have any further work requests, please contact me directly through my Twitter or website!

Offers app development and apps using Flutter and Unity. Includes information on music and videos created by the company. Distribution of images and video materials. We also accept orders for work.
https://mathru.nethttps://mathru.net
title

GitHub Sponsors

Sponsors are always welcome. Thank you for your support!

Developed the katana/masamune framework, which has dramatically improved the overall efficiency of Flutter-based application development.
https://github.comhttps://github.com
title