Multi Form Fields
A powerful Flutter package for managing multiple form fields with built-in debouncing, focus control, and state management. Simplify your form handling with automatic text controller and focus node management.
Features
✨ Automatic Controller Management - No need to manually create and dispose TextEditingControllers
⏱️ Built-in Debouncing - Configurable debounce timers for text input (perfect for search fields and API calls)
🎯 Focus Management - Easy focus control with automatic FocusNode handling
🔑 Type-Safe Keys - Use enums, strings, or any type as field identifiers
🎨 Flexible & Lightweight - Minimal boilerplate, maximum productivity
♻️ Automatic Cleanup - Controllers and focus nodes are automatically disposed
🔄 Real-time & Debounced Callbacks - Handle both immediate and delayed text changes
Getting started
Add this package to your pubspec.yaml:
dependencies:
multi_form_fields: ^1.0.0
Then run:
flutter pub get
Usage
Basic Example
Here's a simple example with an email and password form:
import 'package:flutter/material.dart';
import 'package:multi_form_fields/multi_form_fields.dart';
// Define your form field keys using an enum
enum FormField { email, password }
class LoginForm extends StatefulWidget {
const LoginForm({Key? key}) : super(key: key);
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm>
with MultiFormFieldsMixin<FormField, LoginForm> {
@override
void initState() {
super.initState();
// Initialize controllers for all fields
initControllers(
FormField.values,
withFocusNode: true, // Enable focus management
);
}
@override
void onFieldChanged(FormField key, String value) {
// Called immediately when text changes
print('$key changed to: $value');
}
@override
void onFieldDebounced(FormField key, String value) {
// Called after user stops typing (default 600ms)
print('$key debounced: $value');
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: getController(FormField.email),
focusNode: getFocusNode(FormField.email),
decoration: const InputDecoration(labelText: 'Email'),
),
TextField(
controller: getController(FormField.password),
focusNode: getFocusNode(FormField.password),
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
],
);
}
}
Advanced Example with Custom Debounce
enum SearchField { query, filters, category }
class SearchFormState extends State<SearchForm>
with MultiFormFieldsMixin<SearchField, SearchForm> {
@override
void initState() {
super.initState();
initControllers(
SearchField.values,
debounceDuration: const Duration(milliseconds: 500), // Global debounce
perKeyDebounce: {
// Custom debounce per field
SearchField.query: const Duration(milliseconds: 800),
},
initialValues: {
// Set initial values
SearchField.category: 'All',
},
withFocusNode: true,
);
}
@override
void onFieldDebounced(SearchField key, String value) {
if (key == SearchField.query && value.isNotEmpty) {
// Perform search API call
performSearch(value);
}
}
void performSearch(String query) {
// Your search logic here
}
@override
Widget build(BuildContext context) {
return TextField(
controller: getController(SearchField.query),
focusNode: getFocusNode(SearchField.query),
decoration: const InputDecoration(
labelText: 'Search',
prefixIcon: Icon(Icons.search),
),
);
}
}
Using MultiFormScope for Shared State
Share form state across multiple widgets using MultiFormScope:
class ParentWidget extends StatefulWidget {
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget>
with MultiFormFieldsMixin<FormField, ParentWidget> {
@override
void initState() {
super.initState();
initControllers(FormField.values, withFocusNode: true);
}
@override
Widget build(BuildContext context) {
return MultiFormScope<FormField, ParentWidget>(
mixin: this,
child: Column(
children: [
EmailField(),
PasswordField(),
SubmitButton(),
],
),
);
}
}
// Child widget accessing the form state
class EmailField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final form = context.multipleForm<FormField, ParentWidget>();
return TextField(
controller: form.getController(FormField.email),
focusNode: form.getFocusNode(FormField.email),
decoration: const InputDecoration(labelText: 'Email'),
);
}
}
Programmatically Control Fields
// Set text without triggering callbacks
setText(FormField.email, 'user@example.com', notify: false);
// Set text and trigger callbacks
setText(FormField.email, 'user@example.com', notify: true);
// Get current text value
final email = getText(FormField.email);
// Focus management
requestFocus(FormField.email);
unFocus(FormField.email);
// Check focus state
if (hasFocus(FormField.email)) {
print('Email field has focus');
}
// Change debounce duration dynamically
setDebounceDuration(
FormField.search,
const Duration(milliseconds: 1000),
);
API Reference
MultiFormFieldsMixin
Main mixin that provides form field management capabilities.
Methods
initControllers()- Initialize controllers for specified keysgetController(K key)- Get TextEditingController for a fieldgetFocusNode(K key)- Get FocusNode for a fieldgetText(K key)- Get current text value (returns null if empty)setText(K key, String text, {bool notify})- Set text programmaticallyhasController(K key)- Check if controller existshasFocusNode(K key)- Check if focus node existshasFocus(K key)- Check if field has focusrequestFocus(K key)- Request focus for a fieldunFocus(K key)- Remove focus from a fieldsetDebounceDuration(K key, Duration duration)- Set custom debounce durationdisposeControllers()- Manually dispose all controllersdisposeFocusNodes()- Manually dispose all focus nodes
Callbacks
onFieldChanged(K key, String value)- Called immediately on text changeonFieldDebounced(K key, String value)- Called after debounce delay
Properties
defaultDebounce- Default debounce duration (600ms)
MultiFormScope
InheritedWidget for sharing form state across the widget tree.
MultiFormScope<KeyType, WidgetType>(
mixin: formMixin,
child: YourWidget(),
)
Access via:
context.multipleForm<KeyType, WidgetType>()
FormKey
Type-safe wrapper for form field keys (optional utility class).
const emailKey = FormKey('email');
const passwordKey = FormKey('password');
Best Practices
- Use Enums for Field Keys - Enums provide type safety and better IDE support
- Set Initial Values in initControllers - Avoid setting text in build method
- Use notify: false When Setting Text - Prevent unnecessary callbacks when programmatically updating fields
- Leverage Debouncing for API Calls - Use
onFieldDebouncedfor expensive operations - Clean Focus Management - Always initialize with
withFocusNode: trueif you need focus control
Common Use Cases
- 🔍 Search Forms - Debounced search with real-time results
- 📝 Multi-step Forms - Manage complex forms with many fields
- ✅ Form Validation - Real-time validation with debounced API checks
- 💬 Chat Interfaces - Text input with focus management
- 🎨 Dynamic Forms - Forms with conditional fields
Additional Information
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Issues
If you encounter any issues or have feature requests, please file them on the GitHub issues page.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Developed by Abbosbek Botirjonovich.