form_builder_companion_presenter 0.5.0+1
form_builder_companion_presenter: ^0.5.0+1 copied to clipboard
Implements form_companion_presenter for Flutter FormBuilder (https://pub.flutter-io.cn/packages/flutter_form_builder)
form_builder_companion_presenter #
Ease and simplify your FormBuilder related work with fine application structure.
With form_companion_generator, your boilerplate code will be gone!
If you don't use Flutter FormBuilder, check out form_companion_presenter, which is a brother of this package for flutter's built-in Form.
Features #
- With form_companion_generator, your boilerplate code will be gone!
- Separete "presentation logic" from your
Widgetand make them testable.- Easily and simply bind properties and event-handlers to form fields.
- Remove boring works from your form usage code.
- Enable "submit" button if there are no validation errors.
- Combine multiple validators for single
FormBuilderField.
- Asynchronous validation support with throttling and canceling.
- Provide a best practice to fetch validated value. You don't have to think about
onSaveoronChangeorTextControllerorKey... - State restoration.
Getting Started #
Installation #
Do you read this section in pub.flutter-io.cn? Check above and click "Installing" tab!
- Write a dependency to
form_builder_companion_presenterin yourpubspec.yamland runflutter pub get - Or, run
flutter pub add form_builder_companion_presenter.
Usage #
Prerequisite
Note that this example uses riverpod and form_companion_generator, and it is recommended approach.
- Ensure following packages are added to
dependenciesof yourpubspec.yaml
riverpodriverpod_annotationform_builder_companion_presenter
- Ensure following packages are added to
dev_dependenciesof yourpubspec.yaml
build_runnerriverpod_generatorform_companion_generator
Example:
dependencies:
riverpod: # put favorite version above 2.0.0 here
riverpod_annotation: # put favorite version here
form_builder_companion_presenter: # put favorite version here
...
dev_dependencies:
build_runner: # put favorite version above 2.0.0 here
riverpod_generator: # put favorite version here
form_companion_generator: # put favorite version here
- (Optional) Add
build.yamlin your project's package root (next topubspec.yaml) and configure it (see documentation of build_config and form_companion_generator docs.
Steps
- Declare presenter class.
@riverpod
@formCompanion
class MyPresenter extends _$MyPresenter {
MyPresenter() {
}
@override
FutureOr<$MyPresenterFormProperties> build() async {
}
}
- Declare
within your presenter forCompanionPresenterMixinandFormBuilderCompanionMixinin this order:
@riverpod
@formCompanion
class MyPresenter extends _$MyPresenter
with CompanionPresenterMixin, FormBuilderCompanionMixin {
MyPresenter() {
}
@override
FutureOr<$MyPresenterFormProperties> build() async {
}
}
- Add
initializeCompanionMixin()call with property declaration in the constructor of the presenter. Properties represents values of states which will be input via form fields. They have names and validators, and their type must be same asFormBuilderField's type rather than type of state object property:
MyPresenter() {
initializeCompanionMixin(
PropertyDescriptorBuilder()
..add<String>(
name: 'name',
validatorFactories: [
(context) => FormBuilderValidators.required,
],
)
..add<String>(
name: 'age',
validatorFactories: [
(context) => FormBuilderValidators.required,
(context) => (value) => int.parse(value!) < 0 ? 'Age must not be negative.' : null,
],
)
);
}
Note that there are various extension methods of PropertyDescriptorBuilder to implement initialization easily. In addition, you can specify form field types for generated form factories with extension methods of PropertyDescriptorBuilder which have WithField suffixes.
- Implement
buildto fetch upstream state and fill it as properties' initial state.
@override
FutureOr<$MyPresenterFormProperties> build() async {
final upstreamState = await ref.watch(upstreamStateProvider.future);
return resetProperties(
(properties.copyWith()
..name(upstreamState.name)
..age(upstreamState.age)
).build()
)
}
- Add
partdirective near top of the file whereexample.dartis the file name of this code.
part 'example.fcp.dart';
part 'example.g.dart';
-
Run
build_runner(for example, runflutter pub run build_runner build -d). Provider global property and related types will be created byriverpod_generator, and$MyPresenterFormStatesand related extensions will be created byform_companion_generator. -
Implement
doSubmitoverride method in your presenter. It handle 'submit' action of the entire form.
@override
FutureOr<void> doSubmit(BuildContext context) async {
// Gets a validated input values
String name = properties.values.name;
int age = properties.values.age;
// Calls your business logic here. You can use await here.
...
// Set state to expose for other components of your app.
ref.read(anotherStateProvider).state = AsyncData(MyState(name: name, age: age));
// and more...
}
- Create a widget. We use
ConsumerWidgethere. Note that you must placeFormBuilderandFormBuilderFieldsto separate widget. Note thatstate.fieldshave form field factories generated byform_companion_generatorand their type can be controlled in the presenter:
class MyForm extends StatelessWidget {
@override
Widget build(BuildContext context) => FormBuilder(
child: MyFormFields(),
);
}
class MyFormFields extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: [
state.value.fields.name(
context,
),
state.value.fields.age(
context,
),
ElevatedButton(
onTap: state.value.submit(context),
child: Text('Submit'),
),
],
);
}
}
If you set AutovalidateMode.disabled (default value), you can execute validation in head of your doSubmit() as following:
@override
FutureOr<void> doSubmit(BuildContext context) async{
if (!await validateAndSave(context)) {
return;
}
..rest of code..
}
That's it!
Enable State Restoration #
State restoration improves form input experience because it restores inputting data for the form when the app was killed on background by mobile operating systems. Is it very frustrated if you lose inputting data during open browser to find how to fill the form fields correctly? The browser tends to use large memory, so your app could be terminated frequently.
To enable state restoration, just put FormPropertiesRestorationScope under your FormBuilder like following:
class MyForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final presenter = ref.read(myPresenterProvider.notifier);
return FormBuilder(
child: FormPropertiesRestorationScope(
presenter: presenter,
child: MyFormFields(),
),
);
}
}
Note that if you omit (or specify null for) restorationId of FormPropertiesRestorationScope, string representation of runtimeType of presenter will be used. If you avoid restoration in whole fields, just remove FormPropertiesRestorationScope your widget tree.
More examples? #
See repository. *_form_builder_*.dart files use form_builder_companion_presenter. Note that *_vanilla_form_*.dart files use form_companion_presenter instead.
Components #
This package includes one main construct:
FormBuilderCompanionMixin, which connects flutter's built-inFormBuilderandCompanionPresenterMixIn.
You can use components from form_companion_presenter. See documents for details.
Localization #
Of course, you can localize texts and messages to be shown for users!
For localize built-in validators' messages, you can use .copyWith() method and ParseFailureMessageProvider<T> function to plug in the localization code.
For AsyncValidatorExecutor's text, just specify the localized label via text named parameter of the constructor.
For FormFields which are created in form field factory (.fields in previous example), you can add your localization logic with configuring "templates" in your build.yaml. See form_companion_generator docs for details.
PropertyDescriptor details #
You can customize PropertyDescriptor via optional named parameters of PropertyDescriptorsBuilder's add method or its extension methods. Following table shows parameters of the add method. Note that some of them are common in other (extension) methods.
| name | summary | type | default | note |
|---|---|---|---|---|
P |
Type of the property to be stored. | extends Object |
(required) | Can be accessed via PropertyDescriptor<P, F>.value getter. |
F |
Type of the value in FormField. |
extends Object |
(required) | Can be accessed via getFieldValue and setFieldValue. |
name |
Name of the property. | String |
(required) | Name will be 1) getter names generated by form_companion_generator, 2) keys to get PropertyDescriptor |
validatorFactories |
Factories for normal validations. | List<FormFieldValidatorFactory<F>>? |
[] |
See below for details. |
asyncValidatorFactories |
Factories for asynchronous validations. | List<AsyncValidatorFactory<F>>? |
[] |
See below for details. |
initialValue |
An initial value of the property. | P? |
null |
|
equality |
Custom equality comparer for F. |
Equality<F>? |
null |
If null, values for FormField will be compared with equals() method. |
valueConverter |
Value conversion between P and F. |
ValueConverter<P, F>? |
null |
If null, internal default converters will be used. Note that value conversion will be treated as normal validation implicitly. See below for details. |
enumValues |
(introduced in 0.5) Tells values of the specified enum for state restoration. | Iterable<T> |
(required) | In most cases, E.values static member like Brightness.values. |
valueTraits |
(introduced in 0.5) Specifies additional traits of the value. | PropertyValueTraits? |
null |
null means uses PropertyValueTraits.none. For details, see "Value Traits" section bellow. |
restorableValueFactory |
(introduced in 0.5) Factory to produce RestorableValue<F> for the property. |
RestorableValueFactory<F>? |
null |
If null, restoration will not work. There are some out-of-box built-in factories: stringRestorableValueFactory, intRestorableValueFactory, doubleRestorableValueFactory, boolRestorableValueFactory, bigIntRestorableValueFactory, enumRestorableValueFactory(), enumListRestorableValueFactory(), dateTimeRestorableValueFactory, dateTimeRangeRestorableValueFactory, and rangeValuesRestorableValueFactory. |
Normal Validations #
Normal validations are done by normal validators, and the validators will be constructed via validator factories which are specified in validatorFactories parameter.
validatorFactories parameter's type is List<FormFieldValidatorFactory<F>>?, and FormFieldValidatorFactory<F> is alias of FormFieldValidator<T> Function(ValidatorCreationOptions) function type, where FormFieldValidator<T> is alias of String? Function(T?) function type, which is defined in flutter/widgets.dart library.
The ValidatorCreationOptions contains BuildContext and Locale, which are determined when PropertyDescriptor<P, F>.getValidator() method is called. The validator factories can use these parameters to build their validators, a main use-cases including error message localization and format (such as decimal number or currency) localization.
The contract of FormFieldValidator<T> is same as normal flutter's validators. So, you return null for valid input, or return non-null validation error message for invalid input.
Note that tail of normal validators chain is always implicit validator which try to convert from F to P. It will return conversion failure message as validation result when it will fail to convert value from F to P.
Asynchronous Validation #
Asynchronoous validations are done by asynchronous validators, and the validators will be constructed via validator factories which are specified in asyncValidatorFactories parameter.
asyncValidatorFactories parameter's type is List<AsyncValidatorFactory<F>>?, and AsyncValidatorFactory<F> is alias of AsyncValidator<T> Function(ValidatorCreationOptions) function type. The AsyncValidator<T> is alias of FutureOr<String?> Function(T?, AsyncValidatorOptions) function type.
The ValidatorCreationOptions contains BuildContext and Locale, which are determined when PropertyDescriptor<P, F>.getValidator() method is called. This is same as normal validator factories, so see previous description for it. More importantly, all async validation logics take a second parameter, whih type is AsyncValidatorOptions. The AsyncValidatorOptions object also contains Locale, which is guaranteed to be available when the async validation is called. So, async validators should always use this value instead of the value passed via ValidatorCreationOptions as long as possible.
The contract of AsyncValidator<T> is same as normal flutter's validators except it is wrapped with FutureOr<T>. So, you declare the async validation logic as async, and you return null for valid input, or return non-null validation error message for invalid input.
Note that asynchronous validators chain will be invoked after all normal validators including an implicit validator which try to convert from F to P.
Value Conversion #
Sometimes, a type of stored property value (P) and a type of the value which is edited via FormField<T> (F) are different. For example, if a numeric value is input in text box (such as TextFormField), P should be int (or one of the other numeric types). To handle such cases, valueConverter parameter takes ValueConverter<P, F> object.
ValueConverter<P, F> defines two conversion methods, F? toFieldValue(P? value, Locale locale) and SomeConversionResult<P> toPropertyValue(F? value, Locale locale). For most cases, you just use ValueConverter.fromCallbacks factory method, which takes two functions, PropertyToFieldConverter<P, F> and FieldToPropertyConverter<P, F>, they are conpatible with toFieldValue method and toPropertyValue methods respectively. Furthermore, you should use StringConverter<P>.fromCallbacks when F is String, it provides basic implementation for String conversion.
There are some built-in StringConverters are available:
intStringConverterforintandStringconversion.doubleStringConverterfordoubleandStringconversion.bigIntStringConverterforBigIntandStringconversion.dateTimeStringConverterforDateTimeandStringconversion.uriStringConverterforUriandStringconversion.
You can use StringConverter.copyWith method when you only customize any combination of:
- Conversion failure message.
- It is usually done for message localization.
- Default conversion result for
nullinput fromFormField. - Default
Stringresult fornullinput (initialValueof the property).
So, it is advanced scenario to call StringConverter.fromCallbacks directly, and it is more advanced scenario to call ValueConverter.fromCallbacks directory or extends their converter types. StringConverter.copyWith should cover most cases.
Note that tail of normal validators chain (described above) is always implicit validator which try to convert from F to P. This means that value conversion should be done twice for field value to property value conversion propcess. In addition, when you use text form like field, validation and conversion pipeline should be fired in every charactor input. So, you should implement value convertor that it is light weight and idempotent.
Value Traits #
From 0.5, you can specify PropertyValueTraits for each properties. It affects runtime behavior as following:
| member | effect |
|---|---|
doNotRestore |
If this value is specified, state restoration with form_companion_presenter is disabled for the form field. This is useful the field which input is trivial for users but it can occupy restoration state data. |
sensitive |
If this value is specified, state restoration with form_companion_presenter is disabled for the form field to avoid persist sensitive data in the local device. In addition, form factories which will be generated by form_companion_generator uses true for default values for obscureText parameters of text field based form fields. |
Extension methods #
PropertyDescriptorsBuilder
As described above, there are some extension methods for PropertyDescriptorsBuilder to provide convinient way to define property with commonly used parameters.
(from form_companion_presenter)
| name | summary | parameters | package | defined in | note |
|---|---|---|---|---|---|
string |
Short hand for add<String, String>. |
Mostly same as add but no valueConverter. |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
For text field based form fields and String property. |
boolean |
Short hand for add<bool, bool>. |
Only name and initialValue. |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
For check box like form fields and bool property. Note that default initial value is defined as false rather than null. Use enumerated<T> for tri-state value. |
enumerated<T> |
Short hand for add<T, T> and T is enum. |
Only name and initialValue. |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
For drop down or single selection form fields for enum value. |
integerText |
Short hand for add<int, String>. |
Mostly same as add but there is a stringConverter instead of valueConverter (default is intStringConverter). |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
|
realText |
Short hand for add<double, String>. |
Mostly same as add but there is a stringConverter instead of valueConverter (default is doubleStringConverter). |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
|
bigIntText |
Short hand for add<BigInt, String>. |
Mostly same as add but there is a stringConverter instead of valueConverter (default is bigIntStringConverter). |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
|
uriText |
Short hand for add<Uri, String>. |
Mostly same as add but there is a stringConverter instead of valueConverter (default is uriStringConverter). |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
|
stringConvertible<P> |
Short hand for add<P, String>. |
Mostly same as add but stringConverter (instead of valueConverter) is required. |
form_companion_presenter/form_companion_presenter.dart |
FormCompanionPropertyDescriptorsBuilderExtension |
Use other extension (xxxText) if you can. |
(from this form_builder_companion_presenter)
| name | summary | parameters | package | defined in | note |
|---|---|---|---|---|---|
booleanList |
Short hand for add<List<bool>, List<bool>>. |
Only name and initialValue. |
form_builder_companion_presenter/form_builder_companion_presenter.dart |
FormBuilderCompanionPropertyDescriptorsBuilderExtension |
For check box list like form fields and list of bool property. Typically, each labels correspond to options' indexes. |
enumeratedList<T> |
Short hand for add<List<T>, List<T>> and T is enum. |
Only name and initialValue. |
form_builder_companion_presenter/form_builder_companion_presenter.dart |
FormBuilderCompanionPropertyDescriptorsBuilderExtension |
For multiple selection form fields for enum value. Typically, each labels correspond to enum members (that is, option names). |
dateTime |
Short hand for add<DateTime, DateTime>. |
Mostly same as add but no valueConverter. |
form_builder_companion_presenter/form_builder_companion_presenter.dart |
FormBuilderCompanionPropertyDescriptorsBuilderExtension |
|
dateTimeRange |
Short hand for add<DateTimeRange, DateTimeRange>. |
Mostly same as add but no valueConverter. |
form_builder_companion_presenter/form_builder_companion_presenter.dart |
FormBuilderCompanionPropertyDescriptorsBuilderExtension |
|
rangeValues |
Short hand for add<RangeValues, RangeValues>. |
Mostly same as add but no valueConverter. |
form_builder_companion_presenter/form_builder_companion_presenter.dart |
FormBuilderCompanionPropertyDescriptorsBuilderExtension |
See API doc of FormCompanionPropertyDescriptorsBuilderExtension and API doc of FormBuilderCompanionPropertyDescriptorsBuilderExtension for details.
WithField extension methods
There are some WithField variations for above extension methods. There methods accept additional type parameter TField, which asks for form_companion_generator to use the specified FormField class for the property. So, notice that TField does no effect when you do not use form_companion_generator.
Question Why some extension methods rack of
xxxWithFieldcompanion?A: Because there are no out-of-box alternative form fields for them, and you can use
addWithField<P, F, TField>anyway. If you find that there is a new out-of-box or popular alternative form fields, please file the issue.
PropertyDescriptor
There are also extension methods for PropertyDescriptor, to provide convinient access when you do not use form_companion_generator. These extension methods are defined in CompanionPresenterMixinPropertiesExtension in form_companion_presenter/form_companion_extension.dart library.
See API docs of CompanionPresenterMixinPropertiesExtension for details.
State Restoration in Detail #
As mentioned above, state restoration improves form input experience because it restores inputting data for the form when the app was killed on background by mobile operating systems. Is it very frustrated if you lose inputting data during open browser to find how to fill the form fields correctly? The browser tends to use large memory, so your app could be terminated frequently.
So, every form fields should support restoration, but it is hard to expect all fields implement it. To resolve this problem, form_builder_companion_presenter uses following strategy:
PropertyDescriptor.initialValueremembers "initial value", which is the "field value" of the property if the restored value does not exist.- The validator returned from
PropertyDescriptor.getValidator()remembers validation result. - When there is a restored value,
PropertyDescriptor.initialValuereturns the restored value and it eventually set toinitialValueof the form field. In addition, it schedules validation invocation if the form field had validation error before app termination and is not configured to use auto-validation.
Note that state restoration does not work in Web nand Desktop platforms by Flutter's design.
Breaking Changes #
See [https://github.com/yfakariya/form_companion_presenter/blob/main/BREAKING_CHANGES.md] to check breaking changes.