form_companion_presenter 0.4.0 form_companion_presenter: ^0.4.0 copied to clipboard
Helps building presenter for forms with validations in model layer including asynchronous validations, submittion button availability, etc.
form_companion_presenter #
Ease and simplify your Form
related work with fine application structure.
With form_companion_generator, your boilerplate code will be gone!
If you use Flutter FormBuilder, check outform_builder_companion_presenter, which is a brother of this package for Flutter FormBuilder.
Features #
- With form_companion_generator, your boilerplate code will be gone!
- Separete "presentation logic" from your
Widget
and 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
FormField
.
- Asynchronous validation support with throttling and canceling.
- Provide a best practice to fetch validated value. You don't have to think about
onSave
oronChange
orTextController
orKey
...
Getting Started #
Installation #
Do you read this section in pub.dev? Check above and click "Installing" tab!
- Write a dependency to
form_companion_presenter
in yourpubspec.yaml
and runflutter pub get
- Or, run
flutter pub add form_companion_presenter
.
Usage #
- Ensure following packages are added to
dependencies
of yourpubspec.yaml
riverpod
riverpod_annotation
form_builder_companion_builder
- Ensure following packages are added to
dev_dependencies
of yourpubspec.yaml
build_runner
riverpod_generator
form_companion_generator
Example:
dependencies:
riverpod: # put favorite version above 2.0.0 here
riverpod_annotation: # put favorite version here
form_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
Steps
- Declare presenter class. Note that this example uses riverpod and form_companion_generator, and it is recommended approach.
@riverpod
@formCompanion
class MyPresenter extends AutoDisposeAsyncNotifier<$MyPresenterFormProperties> {
MyPresenter() {
}
@override
FutureOr<$MyPresenterFormProperties> build() async {
}
}
- Declare
with
in your presenter forCompanionPresenterMixin
andFormCompanionMixin
in this order:
@riverpod
@formCompanion
class MyPresenter extends AutoDisposeAsyncNotifier<$MyPresenterFormProperties> {
with CompanionPresenterMixin, FormCompanionMixin {
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 asFormField
's type rather than type of state object property:
MyPresenter() {
initializeCompanionMixin(
PropertyDescriptorBuilder()
..string(
name: 'name',
validatorFactories: [
(context) => (value) => (value ?? '').isEmpty ? 'Name is required.' : null,
],
)
..integerText(
name: 'age',
validatorFactories: [
(context) => (value) => (value ?? '').isEmpty ? 'Age is required.' : null,
(context) => (value) => int.parse(value!) < 0 ? 'Age must not be negative.' : null,
],
)
);
}
- Implement
build
to 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
part
directive near top of the file whereexample.dart
is 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$MyPresenterFormStates
and related extensions will be created byform_companion_generator
. -
Implement
doSubmit
override 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 = getSavedPropertyValue('name');
int age = getSavedPropertyValue('age');
// Calls your business logic here. You can use await here.
...
// Set state to expose for other components of your app.
state = AsyncData(MyState(name: name, age: age));
}
- Create widget. We use
ConsumerWidget
here. Note that you must placeForm
andFormFields
to separate widget:
class MyForm extends StatelessWidget {
@override
Widget build(BuildContext context) => Form(
child: MyFormFields(),
);
}
class MyFormFields extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Name',
),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Age',
),
),
ElevatedButton(
child: Text('Submit'),
),
],
);
}
}
- Set
Form
'sautovalidateMode
.AutovalidateMode.onUserInteraction
is recommended.
class MyForm extends StatelessWidget {
@override
Widget build(BuildContext context) => Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: MyFormFields(),
);
}
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..
}
- Get presenter and state in your form field widget, and bind them to the field and 'submit' button:
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(_presenter);
return Column(
children: [
TextFormField(
key: state.presenter.getKey('name', context),
initialValue: state.getValue('name'),
validator: state.getFieldValidator('name', context),
onSave: state.savePropertyValue('name', context),
decoration: InputDecoration(
labelText: 'Name',
),
),
TextFormField(
key: state.presenter.getKey('age', context),
initialValue: state.getValue('age'),
validator: state.getFieldValidator('age', context),
onSave: state.savePropertyValue('age', context),
decoration: InputDecoration(
labelText: 'Age',
),
),
ElevatedButton(
child: Text('Submit'),
onTap: state.submit(context),
),
],
);
}
**Or, thanks to form_companion_genarator, you can avoid boilerplates as following!
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(_presenter);
return Column(
children: [
state.fields.name(
context,
decoration: InputDecoration(
labelText: 'Name',
),
),
state.fields.age(
context,
decoration: InputDecoration(
labelText: 'Age',
),
),
ElevatedButton(
child: Text('Submit'),
onTap: state.submit(context),
),
],
);
}
That's it!
More examples? #
See repository. *_vanilla_form_*.dart
files use form_companion_presenter
. Note that *_form_builder_*.dart
files use form_builder_companion_presenter instead.
Uses form_companion_generator
to reduce boiler plate code #
You can use form_companion_generator to reduce boiler plate code from previous examples.
To use the generator, follow steps below (see form_companion_generator docs for details):
- After step 3 of previous examples, qualify your presenter class with
@formCompanion
(or@FormCompanion()
) annotation:
@formComapnion
class MyPresenter extends StateNotifier<MyViewState>
with CompanionPresenterMixin, FormCompanionMixin {
...
- Add following packages to
dev_dependencies
of yourpubspec.yaml
build_runner
form_companion_generator
-
(Optional) Add
build.yaml
in your project's package root (next topubspec.yaml
) and configure it (see documentation of build_config and form_companion_generator docs for details). -
Run
flutter pub run build_runner
in your project. Some.fcp.dart
files will be generated. -
(Altered from step 4 of previous example ) Implement
doSubmit
override method in your presenter. It handle 'submit' action of the entire form. Note that you can use typed and named getters to access property values.
@override
FutureOr<void> doSubmit(BuildContext context) async {
// Gets a validated input values
String name = this.name.value!;
int age = this.age.value!;
// Calls your business logic here. You can use await here.
...
// Set state to expose for other components of your app.
state = MyState(name: name, age: age);
}
- (Same as step 5 of previous example) Register your presenter to provider. This example uses riverpod:
final _presenter = StateNotifierProvider<MyPresenter, MyViewState>(
(ref) => MyPresenter(),
);
- (Same as step 6 of previous example) Create widget. We use
ConsumerWidget
here. Note that you must placeForm
andFormFields
to separate widget:
class MyForm extends StatelessWidget {
@override
Widget build(BuildContext context) => Form(
child: MyFormFields(),
);
}
class MyFormFields extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Name',
),
),
TextFormField(
decoration: InputDecoration(
labelText: 'Age',
),
),
ElevatedButton(
child: Text('Submit'),
),
]);
}
}
- (Same as step 7 of previous example) Set
Form
'sautovalidateMode
.AutovalidateMode.onUserInteraction
is recommended.
class MyForm extends StatelessWidget {
@override
Widget build(BuildContext context) => Form(
autovalidateMode: AutovalidateMode.onUserInteraction,
child: MyFormFields(),
);
}
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..
}
- (Altered from step 8 of previous example) Get presenter and state in your form field widget, and bind them to the field and 'submit' button. Note that there are much less code than previous example:
@override
Widget build(BuildContext context, WidgetRef ref) {
final presenter = ref.watch(_presenter.notifier);
return Column(
children: [
presenter.fields.name(context),
presenter.fields.age(context),
ElevatedButton(
child: Text('Submit'),
onTap: presenter.submit(context),
),
],
);
}
Components #
This package includes two main constructs:
CompanionPresenterMixIn
, which is main entry point of thisform_companion_presenter
.FormCompanionMixin
, which connects flutter's built-inForm
andCompanionPresenterMixIn
.
There are advanced constructs for you:
AsyncValidatorExecutor
, which is helper of asynchronous validation.- This is a helper of
FormCompanionPresenterMixIn
, but you can utilize alone.
- This is a helper of
FutureInvoker
, which translatesFuture
based async to completion callback based async.- This is base class of
AsyncValidatorExecutor
, and you can use it as you like.
- This is base class of
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 ParseFailureMessasgeProvider<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 FormField
s 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. |
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 StringConverter
s are available:
intStringConverter
forint
andString
conversion.doubleStringConverter
fordouble
andString
conversion.bigIntStringConverter
forBigInt
andString
conversion.dateTimeStringConverter
forDateTime
andString
conversion.uriStringConverter
forUri
andString
conversion.
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
null
input fromFormField
. - Default
String
result fornull
input (initialValue
of 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.
Extension methods #
PropertyDescriptorsBuilder
As described above, there are some extension methods for PropertyDescriptorsBuilder
to provide convinient way to define property with commonly used parameters.
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. |
See API docs of FormCompanionPropertyDescriptorsBuilderExtension
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
xxxWithField
companion?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.
Implementing Your Own Mixin #
You can implement own mixin for your favorite form field framework. To do so, you should implement following:
- Your
CompanionPresenterFeatures
subtype, which implements actual behavior of the mixin. Override methods which you must to do. - Your
FormStateAdapter
subtype, which wraps actualState
of your favorite form field framework. You just implement the class to wrap the actual state. - Your
CompanionPresenterMixin
subtype as follows:- Declare a field which hold your
CompanionPresenterFeatures
subtype. It should be declared aslate final
because it will be initialized ininitializeCompanionMixin
method override. - Override
presenterFeatures
getter to return the field which is typed as theCompanionPresenterFeatures
subtype. - Override
initializeCompanionMixin
to initialize the field and callsuper.initializeCompanionMixin()
with aproperties
argument. - Override other methods if and only if you should do.
- Declare a field which hold your
For better understand, see source codes of FormCompanionFeatures
, FormStateAdapter
, FormCompanionPresneter
, FormBuilderCompanionFeatures
, FormBuilderStateAdapter
, and FormBuilderCompanionPresenter
.
CompanionPresenterMixin
#
This extension defines helper methods to implement your own mixin related types which were described above. See API docs of CompanionPresenterMixinExtension
for details.