Form companion presenter helps your annoying Form related coding. This project includes three packages:
form_companion_presenter
: Core package, main modules areCompanionPresenterMixin
andFormCompanionMixin
. If you don't want to use Flutter FormBuilder, try use it.form_builder_companion_presenter
: Provides convinient implementation utilizing Flutter FormBuilder package. It is recommended package as long as you don't have any requirement for library usages (there are many form helper packages in pub.dev).form_companion_generator
: Helper tool running withbuild_runner
. This tool generates typed property accessors and form field factories for presenters which are marked with@formCompanion
(or parameterized@FormCompanion
) annotation. This tool also provides some customize points. See ReadMe for details.
CompanionPresenterMixin
provides "properties" of the presenter. The properties are represented as list ofPropertyDescriptor
s, which havename
,validator
, etc. You can use the properties in your presenter as well as in yourWidget
. It enables simple unit testing of validation logics.- If you use it with [FormBuilder],
name
can be used as a value forFormBuilderField.name
. - A validator of
PropertyDescriptor
can contain one or more asynchronous validators as well as one or more validators. Asynchronous validator is useful for validation logics which need to call remote APIs. - You can bind
PropertyDescriptor
toFormField
with convinient methods likegetPropertyValidator
,getKey
, orsavePropertyValue
.
- If you use it with [FormBuilder],
- Additional mixins, namely
FormCompanionMixin
andFormBuilderCompanionMixin
connects betweenCompanionPresenterMixin
and correspond form library.FormCompanionMixin
for flutter's built-inForm
.FormBuilderCompanionMixin
for Flutter FormBuilder.
- Auto-validation support. The companion detects
autoValidateMode
ofForm
orFormBuilder
and respect it in its behaviors. submit
property which is suitable foronTap
of buttons.submit
property will benull
if theForm
is auto-validate mode and not all validation logics have not been completed successfully, so the button will be disabled until validations initiated by auto-validation will be completed.- Supports localization of validation error messages.
- Helper tool
form_companion_generator
, which generates typed property accessors and form field factories for presenters which are marked with@formCompanion
(or parameterized@FormCompanion
) annotation. This tool also provides some customize points. See ReadMe for details.
// Declare presenter for the widget which holds transitive presentation state.
// Of course, you may name it as controller or notifier if you like,
// and you can use any base class which is required by your faviorite library or framework.
class Presenter with CompanionPresenterMixin, FormCompanionMixin {
Presenter(/* parameter to initialize state here */) {
initializeCompanion(
PropertyDescriptorsBuilder()
..string(name: 'name')
..integerText(name: 'age'),
);
}
@override
void doSubmit() {
final name = getSavedPropertyValue<String>('name');
final age = getSavedPropertyValue<int>('age');
// Put presentation layer's logic when "submit" button is tapped/clicked here.
}
}
// In your widget
Widget build(BuildContext context) {
final presenter = /* get presenter here */
final name = presenter.getProperty<String, String>('name');
final age = presenter.getProperty<int, String>('age');
return Row(
children: [
TextFormField(
key: presenter.getKey('name'),
onSaved: name.savePropertyValue,
validator: name.getValidator(context),
),
TextFormField(
key: presenter.getKey('age'),
onSaved: age.savePropertyValue,
validator: age.getValidator(context),
),
ActionButton(
onTap: presenter.submit(context),
)
],
)
}
I know that above code is not so simple. So, you can utilize form_companion_generator
, then the above code can be following:
// Add an annotation to tell the `form_companion_generator` that
// this class should be handled.
@formCompanion
class Presenter with CompanionPresenterMixin, FormCompanionMixin {
Presenter(/* parameter to initialize state here */) {
initializeCompanion(
PropertyDescriptorsBuilder()
..string(name: 'name')
..integerText(name: 'age'),
);
}
@override
void doSubmit() {
final name = this.name.value;
final age = this.age.value;
// Put presentation layer's logic when "submit" button is tapped/clicked here.
}
}
// In your widget
Widget build(BuildContext context) {
final presenter = /* get presenter here */
return Row(
children: [
// Auto-generated FormField factories here.
presenter.fields.name,
presenter.fields.age,
ActionButton(
onTap: presenter.submit(context),
)
],
)
}
See individual README.md
for packages for details. We recommend to use form_builder_companion_presenter as long as you have any reason to avoid flutter_form_builder
package. Also, it is highly recommended to run form_companion_generator
on build_runner
for your projects.
You can customize property behavior with PropertyDescriptorsBuilder
's method parameter including:
- A custom value converter which converts between property type in presenter logic and field type in
FormField
. - One or more validators. Validators can be synchronous or asynchronous.
- For localization, actual parameters are
validatorFactories
andasyncValidatorFactories
, which are list of factory functions which acceptValidatorCreationOptions
and returns (async)validator function.
- For localization, actual parameters are
PropertyDescriptorsBuilder()
..add(
name: 'myProperty',
validatorFactories: [
// Functions which accepts ValidatorCreationOptions, and then returns `String? Function(String?)`
(options) => ...,
// This is same signature for FormBuilderValidators.
FormBuilderValidators.required,
],
asyncValidatorFactories: [
// Functions which accepts ValidatorCreationOptions, and then returns `Future<String?> Function(String?, AsyncValidatorOptions)`
(options) => ...,
]
)
You can utilize flutter_form_builder
as helper library by mixing FormBuilderCompanionMixin
instead of FormCompanionMixin
.
You also be able to use your preferred library by implementing custom mixin.
class Presenter with CompanionPresenterMixin, FormBuilderCompanionMixin {
...
}
AsyncValidationIndicator
is helper widget to indicate asynchronous validation is in progress.
In presenter code, you specify async validator factory:
MyPresenter() {
initializeCompanionMixin(
PropertyDescriptorsBuilder()
...
..string(
name: 'someProperty'
...
asyncValidatorFactories: [
// put async validator factory here
],
...
)
....
Now, you can use AsyncValidationIndicator
for someProperty
as following.
TextFormField(
...
decoration: InputDecoration(
...
suffix: AsyncValidationIndicator(
presenter: presenter,
propertyName: 'someProperty',
),
),
),
In most cases, you want to use presenter with some state/dependency management framework, and they require that the presenter do additional work when the property value is changed. To implement it, CompanionPresenterMixin
exposes onPropertiesChanged
template method. You can presenter's state change there for example.
If you use ChangeNotifier
, onPropertiesChanged
should be following:
@override
void onPropertiesChanged(FormProperties newProperties) {
notifyListeners();
}
If you use StateNotifier
of state_notifier
, onPropertiesChanged
should be following:
@override
void onPropertiesChanged(FormProperties newProperties) {
state = newProperties;
}
Notifier
and AsyncNotifier
(and their subtypes) are special because they initialize themselves in build()
method instead of their constructor.
In addition, in most cases, they subscribe dependent state change on build()
and reflect them to the presenter state. To handle such situation, you can use CompanionPresenterMixin.resetPropertiesState
method, or resetProperties
extension method which can be generated by form_companion_generator
. As usual, form_companion_generator
is recommended, so following sample shows resetProperties
usage. Note that $MyPresenterFormProperties
is generated typed FormProperties
class by form_companion_generator
, and Notifier
and AsyncNotifier
do not require override of onPropertiesChanged
because state
is set by return value of the build
method.
@formCompanion
@riverpod
class MyPresenter extends _$MyPresenter
with CompanionPresenterMixin, FormBuilderCompanionMixin {
...
@override
FutureOr<$MyPresenterFormProperties> build() async {
final savedState = await ref.watch(anotherSavedStateFutureProvider.future);
final builder = properties.copyWith();
// ... setup `builder` with `savedState` here ...
builder.somePropety(savedState.someProperty);
...
return resetProperties(builder.build());
}
}
If you use provider or riverpod, you can use StateNotifier
(or Notifier
/AsyncNotifier
) and the typed FormProperties
generated by form_companion_generator
.
At first, run form_companion_generator
and set the type (its name should be ${presenter type name}FormProperties
, remember first charactor is dollar sign) as type parameter of StateNotifier
(or Notifier
/AsyncNotifier
). You can watch state change as typed FormProperties
change as well as you can call companion presenter methods from the state.
The following examples are code with riverpod, please check "Work with Notifier
/AsyncNotifier
of riverpod" section above, too:
@formCompanion
@riverpod
class MyPresenter extends _$MyPresenter
with CompanionPresenterMixin, FormBuilderCompanionMixin {
...
}
// in ConsumerWidget
@override
Widget build(BuildContext context, WidgetRef ref) {
final presenter = ref.read(myPresenterProvider.notifier);
final state = ref.watch(myPresenterProvider);
if (state is! AsyncData<$MyPresenterFormProperties>) {
// loading or error state handling here...
}
// actual widget building here...
}
There are many helper extensions to define properties including string
, integerText
, boolean
, etc. And you can specify FormField
class as generic argument with extension methods end with WithField
suffix.
PropertyDescriptorsBuilder()
// String typed property suitable for String typed FormField.
// Actual form field type depends for which companion you can use (`FormCompanionMixin` or `FormBuilderCompanionMixin`)
..string(name: 'name')
// int typed property suitable for String typed FormField.
// Actual form field type depends for which companion you can use (`FormCompanionMixin` or `FormBuilderCompanionMixin`)
..integerText(name: 'age')
// If you use form_builder_companion_presenter, you can use `DateTime` (`FormBuilderDatePicker` will be used).
// Note that you must mix `FormBuilderCompanionMixin`.
..dateTime(name: 'birthDay')
// Customize form field type for enum (default is `DropdownButtonFormField` or `FormBuilderChoiceChip`)
..enumeratedWithField<Sex, FormBuilderChoiceChip<Sex>>(name: 'sex', enumValues: Sex.values),
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 Form
like following:
class MyForm extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final presenter = ref.read(myPresenterProvider.notifier);
return Form(
child: FormPropertiesRestorationScope(
presenter: presenter,
child: MyFormFields(),
),
);
}
}
This project assumes that you use following packages to build your form and logic. Note that an original author of this project does not relates to these packages.
See example for example sources which utilize above packages.