DEV Community

Cover image for CustomFormField - Custom Widgets Series
Abdur Rafay Saleem
Abdur Rafay Saleem

Posted on

CustomFormField - Custom Widgets Series

Welcome to another article for my Custom Widgets Series. In this series I share some tips and code about creating reusable and advanced flutter widgets. These widgets are used by me and many professionals out there in production level flutter apps. Now they are available to you!

Table of Contents

  1. Introduction
  2. Usage
  3. Code
  4. Credits

Introduction

Are you tired of dealing with the complexities of form fields in Flutter? Do you wish there was a simpler way to handle form validation, error styling, and value changes without writing tons of boilerplate code or messing with the Form API? I designed my own custom widget to make form handling in Flutter a breeze.

I recently shared some screenshots of forms from my Famony app on X(twitter).

These forms contained validations for different field types like dropdowns, textfields, date picker, gender cards etc. You can see in the image below:

Custom form validations in flutter

Many people inboxed me asking how I was able to do that since not every widget has validations and it gets very messy to sync them all.

CustomFormField ✨

Introducing the CustomFormField widget, wrapper around FormField widget that takes in validation, onSaved, onChanged callbacks etc.

With CustomFormField, you can easily manage form validation, error display, and value changes with minimal effort. No more struggling with the default FormField API to achieve basic functionality. It provides a clean and intuitive way to handle form fields, allowing you to focus on building great user experiences.

It has built-in support for validation errors which you can style as you want. Just wrap any child to add validation without worrying about form reactive logic. You can see the it in the example below.

CustomFormField usage

CustomValueListener

The CustomFormField needs a way to keep the form in sync and allow updating it from anywhere using a controller. Since the field can be of dynamic nature, the most suitable option for a controller was a ValueNotifier<T>, where <T> is the type you pass to your CustomFormField<T>. In order to listen to this custom controller we needed a custom listener widget as well.

The CustomValueListener widget is essential for efficiently managing state changes in the CustomFormField. It listens to a ValueNotifier and triggers updates whenever the value changes, ensuring that the form field's state is kept in sync. This separation of concerns simplifies the code, making it cleaner and more maintainable. Additionally, it handles the lifecycle of the listener, adding it during initialization and removing it upon disposal, which prevents memory leaks and ensures optimal performance. This approach allows for a more responsive and dynamic form handling experience in Flutter.

Code

Here is the crux of it all, the code for this amazing widget. I decided to place it at the end to avoid confusion, because it was important to see the usage and benefits before the implementation. I have included it as gist so I can update it in the future, if needed.

import 'package:flutter/material.dart';
// Helpers
import '../../helpers/constants/constants.dart';
// Widgets
import 'custom_value_listener.dart';
typedef OnChangedCallback<T> = void Function(T?);
typedef OnValidateCallback<T> = String? Function(T?)?;
typedef OnSavedCallback<T> = void Function(T?)?;
class CustomFormErrorStyle {
/// The alignment of the error text.
final Alignment errorAlign;
/// The flag used to enable/disable error border.
final bool showErrorBorder;
/// The flag used to enable/disable focused border.
final bool showErrorMessage;
/// The style used for displaying error text.
final TextStyle? textStyle;
const CustomFormErrorStyle({
this.errorAlign = Alignment.centerRight,
this.showErrorBorder = false,
this.showErrorMessage = true,
this.textStyle = const TextStyle(
fontSize: 14,
color: AppColors.redColor,
),
});
}
/// [T] is any type of value that is passed to the [onSaved], [onChanged],
/// and [validator] callback.
class CustomFormField<T> extends StatelessWidget {
/// An optional method to call with the final value when the
/// form is saved via [FormState.save].
final OnSavedCallback<T> onSaved;
/// An optional method that validates an input. Returns an error
/// string to display if the input is invalid, or null otherwise.
final OnValidateCallback<T> validator;
/// An optional method to call with the value when the
/// field is changed
final OnChangedCallback<T>? onChanged;
final ValueNotifier<T> controller;
/// The style used for displaying error.
final CustomFormErrorStyle errorStyle;
/// The width of the error text.
final double? width;
/// The function used to build the widget
final FormFieldBuilder<T> builder;
const CustomFormField({
required this.controller,
required this.builder,
super.key,
this.onSaved,
this.width,
this.onChanged,
this.validator,
this.errorStyle = const CustomFormErrorStyle(),
});
@override
Widget build(BuildContext context) {
return FormField<T>(
initialValue: controller.value,
validator: validator,
onSaved: onSaved,
builder: (state) {
return Column(
children: [
// Display field
CustomValueListener(
controller: controller,
onChanged: () {
state
..didChange(controller.value)
..validate();
onChanged?.call(controller.value);
},
child: builder(state),
),
// Error text
if (state.hasError && errorStyle.showErrorMessage) ...[
Insets.gapH3,
SizedBox(
width: width,
child: Align(
alignment: errorStyle.errorAlign,
child: Text(
state.errorText!,
style: errorStyle.textStyle,
),
),
),
],
],
);
},
);
}
}
class CustomValueListener<T> extends StatefulWidget {
final Widget child;
final ValueNotifier<T> controller;
final VoidCallback onChanged;
const CustomValueListener({
required this.child,
required this.controller,
required this.onChanged,
super.key,
});
@override
_CustomValueListenerState createState() => _CustomValueListenerState();
}
class _CustomValueListenerState extends State<CustomValueListener> {
@override
void initState() {
super.initState();
widget.controller.addListener(widget.onChanged);
}
@override
void didUpdateWidget(covariant CustomValueListener oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller.removeListener(widget.onChanged);
widget.controller.addListener(widget.onChanged);
}
}
@override
void dispose() {
widget.controller.removeListener(widget.onChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
// 1. wrap any child
CustomFormField<T?>(
controller: _controller!,
width: width,
validator: validator,
onSaved: onSaved,
onChanged: onChanged,
errorStyle: errorStyle,
builder: _builder,
)
// 2. Wrap multiple formfields in a form widget
Form(
key: formKey,
child: Column(
children: [
CustomFormField<int>(...),
CustomFormField<DateTime>(...),
CustomFormField<String>(...),
]
),
)
// 3. Run all form fields' validators
if (!formKey.currentState!.validate()) return;
...
view raw usage.dart hosted with ❤ by GitHub

Credits

If you like it, please keep following me as I have around 40 such widgets and I will post many more of these.

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up