FormFieldAdapter

An interactive, focus-aware FormField wrapper designed for Flutter.
FormFieldAdapter simplifies turning any custom input widget (such as rating selectors,
multi-select tag chips, map pin pickers, or image uploads) into a fully integrated form field with
validation, animated visual feedback, and zero boilerplate.
Features
- State-Based Animated Decorations: Smoothly morphs between four custom
Decorationstates ( Normal, Focused, Error, and Focused Error). - Shake Animation: Naturally shakes the input container horizontally when validation fails to instantly capture user attention. Skipped automatically when the system's reduce-motion setting is on.
- Haptic Feedback: Triggers a subtle, light device vibration whenever an error is first caught.
- Focus Node Integration: Listens to a standard Flutter
FocusNodeto trigger active/idle styling changes. You attach the same node to aFocuswidget insidebuilder(see below). - Accessible Errors: Error text is announced to screen readers as a live region.
- Flexible Error Placement: Choose whether standard error messages should appear above the field, below the field, or be hidden entirely (to display custom alert layouts instead).
- Flexible Layout Alignment: Align the entire wrapper and its error text to the
start,center, orendto support centered widgets like profile avatars and rating bars. - Automatic Theme Integration: Falls back to your app's
ThemeData(error and primary colors, andinputDecorationTheme.errorStylefor the error text) if custom values aren't passed. - Works with a plain
Form: No extra form wrapper or dependencies. RTL layouts are supported. - Universal Compatibility: Works on mobile, tablet, desktop, and web.
Installation
Add form_field_adapter to your pubspec.yaml dependencies:
dependencies:
form_field_adapter: ^1.0.2
And run:
flutter pub get
Quick Start
Wrap any interactive widget inside the FormFieldAdapter to connect it with your parent Form:
import 'package:flutter/material.dart';
import 'package:form_field_adapter/form_field_adapter.dart';
class RatingForm extends StatefulWidget {
const RatingForm({super.key});
@override
State<RatingForm> createState() => _RatingFormState();
}
class _RatingFormState extends State<RatingForm> {
final _formKey = GlobalKey<FormState>();
final _focusNode = FocusNode();
int _userRating = 0;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
FormFieldAdapter<int>(
initialValue: _userRating,
focusNode: _focusNode,
validator: (value) =>
(value == null || value < 3) ? 'Must rate at least 3 stars!' : null,
onSaved: (value) => _userRating = value ?? 0,
builder: (state) {
// Attach the same focus node here, or the focused styles never apply.
return Focus(
focusNode: _focusNode,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(5, (index) {
return IconButton(
icon: Icon(
index < (state.value ?? 0) ? Icons.star : Icons.star_border,
color: Colors.amber,
),
onPressed: () {
state.didChange(index + 1);
_focusNode.requestFocus();
},
);
}),
),
);
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
debugPrint('Validation passed! Rating is $_userRating');
}
},
child: const Text('Submit'),
),
],
),
);
}
}
Advanced Customization
For full-control designs, wrap your custom widgets inside a builder method to satisfy your design requirements:
import 'package:flutter/material.dart';
import 'package:form_field_adapter/form_field_adapter.dart';
Widget buildCustomAdapter(FocusNode myFocusNode) {
return FormFieldAdapter<String?>(
focusNode: myFocusNode,
errorPosition: ErrorPosition.top,
// Places the error text above the input
enableHaptics: true,
// Enable device vibrations
enableShake: true,
// Enable failure shake animation
// Design completely custom border styles
normalDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
focusedDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.deepPurple, width: 2),
),
errorDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red, width: 1.5),
),
builder: (state) {
return const SizedBox.shrink(); // Replace with your custom widget
},
);
}
Properties
| Property | Type | Default | Description |
|---|---|---|---|
builder |
Widget Function(FormFieldState<T>) |
Required | Renders your custom input widget. |
initialValue |
T? |
null |
Initial value of the field. Restored by FormState.reset(). |
validator |
String? Function(T?)? |
null |
Returns an error message, or null when the value is valid. |
onSaved |
void Function(T?)? |
null |
Called by FormState.save(). |
autoValidateMode |
AutovalidateMode? |
AutovalidateMode.onUserInteraction |
Spelled differently from Flutter's autovalidateMode; the framework default is disabled. |
enabled |
bool |
true |
Forwarded to FormField: when false, automatic validation is off, but FormState.validate() still validates it. No separate styling. |
focusNode |
FocusNode? |
null |
Listened to for focus changes. Attach the same node to a Focus widget in builder. |
normalDecoration |
Decoration? |
null |
Style applied when the field is valid and not focused. |
focusedDecoration |
Decoration? |
null |
Style applied when the field is valid and focused. Falls back to normalDecoration. |
errorDecoration |
Decoration? |
null |
Style applied when the field has a validation error and is not focused. |
focusedErrorDecoration |
Decoration? |
null |
Style applied when the field has an error and is focused. Falls back to errorDecoration. |
decorationPlacement |
DecorationPlacement |
DecorationPlacement.background |
Renders decoration behind (background) or as a stack overlay (foreground). |
enableShake |
bool |
true |
Triggers a horizontal shake on validation error. Skipped when reduce-motion is on. |
enableHaptics |
bool |
true |
Triggers light haptic feedback on devices on validation error. |
animationDuration |
Duration |
200ms |
Transition duration between the decoration states. |
crossAxisAlignment |
CrossAxisAlignment |
CrossAxisAlignment.start |
Aligns the outer layout container (start, center, or end). |
errorTextAlign |
TextAlign? |
null |
Custom error text alignment. Automatically matches crossAxisAlignment if left null. |
errorPosition |
ErrorPosition |
ErrorPosition.bottom |
Location of the error message (top, bottom, or none). |
errorTextStyle |
TextStyle? |
null |
Typography for the validation error message. |
errorPadding |
EdgeInsetsGeometry? |
null |
Custom margins around the error message. Default: 6 px above/below and 4 px at the start. |
errorBuilder |
Widget Function(BuildContext, String)? |
null |
Completely custom render callback for the error layout. |
Tip: keep the border widths of your custom decorations equal across states. A
Borderadds to the field's size, so different widths make the layout jump when focus or error state changes.
Platforms Supported
- iOS
- Android
- Web
- macOS
- Windows
- Linux
Contributions Welcome
Found a bug or want to improve this widget? Open an issue or pull request on GitHub: github.com/hbsgujjar111/form_field_adapter
License
This project is licensed under the MIT License - see the LICENSE file for details.