In Flutter development, as application scales grow, properly encapsulating custom widgets and reusable components becomes crucial. Well-designed components can significantly improve code reusability, reduce maintenance costs, and ensure UI consistency. This lesson will explore the design principles, implementation methods, and data transfer mechanisms for custom widgets in depth.
I. Principles of Encapsulating Reusable Components
Designing high-quality reusable components requires following core principles that help create flexible, robust, and maintainable components:
1. Single Responsibility Principle
Each component should focus on completing a single function. Avoid designing "all-purpose components." For example, a button component shouldn't handle form validation logic, and a list component shouldn't contain specific list item rendering logic.
Anti-pattern: A component that handles both network requests and UI display
Good practice: Separate network requests from UI display; UI components only handle data presentation
2. High Cohesion, Low Coupling
- High cohesion: Related functions within a component should be tightly integrated, forming an organic whole
- Low coupling: Components should communicate through well-defined interfaces, minimizing direct dependencies
3. Configuration Flexibility
Adapt components to different scenarios through parameter configuration, while balancing flexibility and complexity:
- Provide reasonable default values to reduce usage costs
- Key properties should be configurable, while secondary properties can be fixed
- Use bool, enum, and other types to limit configuration options and avoid misuse
4. Extensibility
Design with extension points to facilitate future functionality expansion:
- Allow custom child components through child or builder parameters
- Extend base component functionality through inheritance or composition
- Avoid hardcoding business logic
5. Consistency and Recognizability
- Maintain component style consistency with the overall application design
- Component behavior should meet user expectations (e.g., button click feedback)
- Components with similar functionality should maintain consistent API design
6. Testability
- Components should be easy to instantiate and test
- Avoid creating global state within components
- Critical logic should be testable independently
II. Basics of Custom Widgets
There are two main ways to create custom components in Flutter: composing existing widgets and creating custom RenderObjects. For most scenarios, we should prioritize composition as it's simpler and meets most needs.
1. Choosing Between StatelessWidget and StatefulWidget
- StatelessWidget: Suitable for stateless components or components whose state is managed by parent components, such as static display components and pure UI components
- StatefulWidget: Suitable for components with internal state management, such as counters and expandable/collapsible panels
Recommendation: Prefer StatelessWidget when possible, elevating state to parent components or state management frameworks to make components easier to test and reuse.
2. Component Naming Conventions
- Use PascalCase, such as PrimaryButton, UserProfileCard
- Names should accurately describe component functionality, avoiding excessive abstraction or generality
- For components with similar basic functions but different styles, use consistent prefixes, such as PrimaryButton, SecondaryButton
III. Custom Widget Examples
1. Example 1: Button with Loading State (StatefulWidget)
This button component supports loading states, disabled states, custom styles, and other features, suitable for form submission, data requests, and similar scenarios.
import 'package:flutter/material.dart';
/// Button component with loading state
/// Supports normal, loading, and disabled states
/// Customizable colors, rounded corners, text styles, etc.
class LoadingButton extends StatefulWidget {
/// Button text
final String text;
/// Click callback
final VoidCallback? onPressed;
/// Whether in loading state
final bool isLoading;
/// Button primary color
final Color? color;
/// Text color
final Color textColor;
/// Disabled state color
final Color disabledColor;
/// Disabled state text color
final Color disabledTextColor;
/// Button border radius
final double borderRadius;
/// Button padding
final EdgeInsetsGeometry padding;
/// Loading indicator color
final Color indicatorColor;
LoadingButton({
super.key,
required this.text,
this.onPressed,
this.isLoading = false,
this.color,
this.textColor = Colors.white,
this.disabledColor = Colors.grey,
this.disabledTextColor = Colors.grey,
this.borderRadius = 8.0,
this.padding = const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
this.indicatorColor = Colors.white,
}) : assert(text.isNotEmpty, "Button text cannot be empty");
@override
State<LoadingButton> createState() => _LoadingButtonState();
}
class _LoadingButtonState extends State<LoadingButton> {
// Whether the button is clickable
bool get _isEnabled => widget.onPressed != null && !widget.isLoading;
@override
Widget build(BuildContext context) {
// Get the primary color from the theme as the default color
final theme = Theme.of(context);
final buttonColor = widget.color ?? theme.primaryColor;
return ElevatedButton(
onPressed: _isEnabled ? widget.onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: _getButtonColor(buttonColor),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(widget.borderRadius),
),
padding: widget.padding,
disabledBackgroundColor: widget.disabledColor,
),
child: _buildButtonContent(buttonColor),
);
}
// Get button color based on state
Color _getButtonColor(Color defaultColor) {
if (widget.isLoading) {
return defaultColor.withOpacity(0.8);
}
return defaultColor;
}
// Build button content (text or loading indicator)
Widget _buildButtonContent(Color buttonColor) {
if (widget.isLoading) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(widget.indicatorColor),
),
),
const SizedBox(width: 8),
Text(
"Loading...",
style: TextStyle(
color: _isEnabled ? widget.textColor : widget.disabledTextColor,
),
),
],
);
}
return Text(
widget.text,
style: TextStyle(
color: _isEnabled ? widget.textColor : widget.disabledTextColor,
),
);
}
}
// Usage example
class LoadingButtonDemo extends StatefulWidget {
const LoadingButtonDemo({super.key});
@override
State<LoadingButtonDemo> createState() => _LoadingButtonDemoState();
}
class _LoadingButtonDemoState extends State<LoadingButtonDemo> {
bool _isLoading = false;
void _handlePress() async {
// Simulate network request
setState(() => _isLoading = true);
await Future.delayed(const Duration(seconds: 2));
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Loading Button Demo')),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
LoadingButton(
text: "Submit",
onPressed: _handlePress,
isLoading: _isLoading,
),
const SizedBox(height: 20),
LoadingButton(
text: "Disabled State",
onPressed: null, // Button is disabled when onPressed is null
color: Colors.grey,
),
const SizedBox(height: 20),
LoadingButton(
text: "Custom Style",
onPressed: () {},
color: Colors.purple,
borderRadius: 20,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
),
],
),
),
);
}
}
2. Example 2: Common App Bar (StatelessWidget)
Implement a highly customizable app bar component that supports left/right buttons, custom title styles, background color settings, and other features.
import 'package:flutter/material.dart';
/// Common app bar component
/// Supports custom title, left/right buttons, background color, etc.
class CommonAppBar extends StatelessWidget implements PreferredSizeWidget {
/// Title text
final String title;
/// Title widget, takes precedence over title
final Widget? titleWidget;
/// Left button
final Widget? leading;
/// List of right buttons
final List<Widget>? actions;
/// Background color
final Color? backgroundColor;
/// Title text style
final TextStyle? titleStyle;
/// Shadow elevation
final double elevation;
/// Whether title is centered
final bool centerTitle;
/// Left button click callback (only effective when leading is not customized)
final VoidCallback? onLeadingPressed;
const CommonAppBar({
super.key,
this.title = "",
this.titleWidget,
this.leading,
this.actions,
this.backgroundColor,
this.titleStyle,
this.elevation = 4.0,
this.centerTitle = true,
this.onLeadingPressed,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppBar(
title: titleWidget ??
Text(
title,
style: titleStyle ??
TextStyle(
color: theme.appBarTheme.titleTextStyle?.color ?? Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
leading: leading ?? _buildDefaultLeading(context),
actions: actions,
backgroundColor: backgroundColor ?? theme.appBarTheme.backgroundColor,
elevation: elevation,
centerTitle: centerTitle,
);
}
// Build default leading button (back button)
Widget? _buildDefaultLeading(BuildContext context) {
// Don't show back button if it's the first page in navigation stack
if (Navigator.canPop(context)) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: onLeadingPressed ?? () => Navigator.pop(context),
);
}
return null;
}
// Define app bar height, using default 56.0
@override
Size get preferredSize => const Size.fromHeight(56.0);
}
// Usage example
class CommonAppBarDemo extends StatelessWidget {
const CommonAppBarDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CommonAppBar(
title: "Home Page",
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print("Search"),
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () => print("More"),
),
],
),
body: const Center(
child: Text("Page content"),
),
);
}
}
// Custom title style example
class StyledAppBarDemo extends StatelessWidget {
const StyledAppBarDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CommonAppBar(
titleWidget: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.star, color: Colors.yellow),
SizedBox(width: 8),
Text("Custom Title"),
],
),
backgroundColor: Colors.purple,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () => print("Menu"),
),
),
body: const Center(
child: Text("Page with custom title"),
),
);
}
}
IV. Component Parameter Validation and Default Values
Good parameter design is key to component usability. Reasonable default values reduce usage costs, while strict parameter validation helps catch errors early.
1. Required vs. Optional Parameters
Use the required keyword to mark mandatory parameters, allowing the compiler to help check parameter completeness:
class CustomButton extends StatelessWidget {
// Required parameters
final String text;
final VoidCallback onPressed;
// Optional parameters
final Color? color;
// Mark required parameters with 'required'
const CustomButton({
super.key,
required this.text,
required this.onPressed,
this.color,
});
// ...
}
2. Setting Default Values
Provide reasonable default values for optional parameters to reduce component complexity:
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final Color color;
final double borderRadius;
final EdgeInsets padding;
const CustomButton({
super.key,
required this.text,
required this.onPressed,
// Provide default values
this.color = Colors.blue,
this.borderRadius = 8.0,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
});
// ...
}
3. Parameter Validation
Use assert statements for parameter validation during development to catch errors early:
class Avatar extends StatelessWidget {
final String url;
final double radius;
const Avatar({
super.key,
required this.url,
this.radius = 24.0,
}) :
// Validate that radius must be positive
assert(radius > 0, "radius must be greater than 0"),
// Validate that url is not empty
assert(url.isNotEmpty, "url cannot be empty");
@override
Widget build(BuildContext context) {
return ClipOval(
child: Image.network(
url,
width: radius * 2,
height: radius * 2,
fit: BoxFit.cover,
),
);
}
}
4. Advanced Parameter Validation
For complex parameter validation logic, perform checks in initState or build methods:
class RangeSelector extends StatefulWidget {
final double min;
final double max;
final double value;
const RangeSelector({
super.key,
required this.min,
required this.max,
required this.value,
});
@override
State<RangeSelector> createState() => _RangeSelectorState();
}
class _RangeSelectorState extends State<RangeSelector> {
@override
void initState() {
super.initState();
_validateParams();
}
// Complex parameter validation
void _validateParams() {
if (widget.min >= widget.max) {
throw FlutterError("min must be less than max: min=${widget.min}, max=${widget.max}");
}
if (widget.value < widget.min || widget.value > widget.max) {
throw FlutterError("value must be within [min, max] range: value=${widget.value}, min=${widget.min}, max=${widget.max}");
}
}
// ...
}
V. Using InheritedWidget for Cross-Layer Data Transfer
In complex applications, component nesting can become very deep, making data passing through constructors cumbersome. InheritedWidget provides an efficient way to transfer data across layers, allowing child components to directly access data from upper layers.
1. InheritedWidget Basics
InheritedWidget is a special type of Widget that can pass data down the widget tree, allowing child components to access this data through BuildContext.
// 1. Create custom InheritedWidget
class ThemeData {
final Color primaryColor;
final Color secondaryColor;
final TextStyle textStyle;
ThemeData({
required this.primaryColor,
required this.secondaryColor,
required this.textStyle,
});
}
class AppTheme extends InheritedWidget {
final ThemeData data;
const AppTheme({
super.key,
required this.data,
required super.child,
});
// Provide static method for child components to access
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
// Whether to notify dependent child components when data changes
@override
bool updateShouldNotify(AppTheme oldWidget) {
return data.primaryColor != oldWidget.data.primaryColor ||
data.secondaryColor != oldWidget.data.secondaryColor ||
data.textStyle != oldWidget.data.textStyle;
}
}
2. Using in Widget Tree
// 2. Provide data at the top of the widget tree
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// Create theme data
final themeData = ThemeData(
primaryColor: Colors.blue,
secondaryColor: Colors.green,
textStyle: const TextStyle(
color: Colors.black87,
fontSize: 16,
),
);
// Provide InheritedWidget
return AppTheme(
data: themeData,
child: const MaterialApp(
home: HomePage(),
),
);
}
}
// 3. Deep nested component accessing data
class DeepNestedWidget extends StatelessWidget {
const DeepNestedWidget({super.key});
@override
Widget build(BuildContext context) {
// Get theme data
final appTheme = AppTheme.of(context);
if (appTheme == null) {
return const Text("Theme data not found");
}
return Container(
color: appTheme.data.secondaryColor,
padding: const EdgeInsets.all(16),
child: Text(
"Text using theme style",
style: appTheme.data.textStyle,
),
);
}
}
// Intermediate component (no need to pass data)
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("InheritedWidget Example")),
body: const Center(
child: IntermediateWidget(),
),
);
}
}
class IntermediateWidget extends StatelessWidget {
const IntermediateWidget({super.key});
@override
Widget build(BuildContext context) {
return const DeepNestedWidget();
}
}
3. Dynamically Updating InheritedWidget Data
InheritedWidget itself is immutable. To achieve dynamic data updates, you need to combine it with StatefulWidget:
class ThemeProvider extends StatefulWidget {
final Widget child;
const ThemeProvider({
super.key,
required this.child,
});
// Provide static method to get state
static _ThemeProviderState of(BuildContext context) {
return context.findAncestorStateOfType<_ThemeProviderState>()!;
}
@override
State<ThemeProvider> createState() => _ThemeProviderState();
}
class _ThemeProviderState extends State<ThemeProvider> {
late ThemeData _themeData;
@override
void initState() {
super.initState();
// Initialize theme data
_themeData = ThemeData(
primaryColor: Colors.blue,
secondaryColor: Colors.green,
textStyle: const TextStyle(
color: Colors.black87,
fontSize: 16,
),
);
}
// Method to toggle theme
void toggleTheme() {
setState(() {
_themeData = _themeData.primaryColor == Colors.blue
? ThemeData(
primaryColor: Colors.purple,
secondaryColor: Colors.orange,
textStyle: const TextStyle(
color: Colors.white,
fontSize: 16,
),
)
: ThemeData(
primaryColor: Colors.blue,
secondaryColor: Colors.green,
textStyle: const TextStyle(
color: Colors.black87,
fontSize: 16,
),
);
});
}
@override
Widget build(BuildContext context) {
return AppTheme(
data: _themeData,
child: widget.child,
);
}
}
// Using dynamic theme
class ThemedButton extends StatelessWidget {
const ThemedButton({super.key});
@override
Widget build(BuildContext context) {
final appTheme = AppTheme.of(context);
return ElevatedButton(
onPressed: () {
// Toggle theme
ThemeProvider.of(context).toggleTheme();
},
style: ElevatedButton.styleFrom(
backgroundColor: appTheme?.data.primaryColor,
),
child: Text(
"Toggle Theme",
style: TextStyle(
color: appTheme?.data.textStyle.color,
),
),
);
}
}
4. InheritedWidget Considerations
- Performance considerations: The updateShouldNotify method should accurately determine if data has actually changed to avoid unnecessary rebuilds
- Don't overuse: For simple scenarios, passing data through constructors is more straightforward
- Data types: InheritedWidget is suitable for passing globally-shared data like configurations, themes, and user information
- Dependency management: Using dependOnInheritedWidgetOfExactType establishes a dependency relationship that triggers rebuilds when data changes; using getInheritedWidgetOfExactType does not establish this relationship
VI. Component Communication Methods
In complex applications, components need to communicate with each other. Flutter provides various component communication methods:
1. Parent-Child Component Communication
- Parent to child: Pass through constructor parameters
- Child to parent: Pass through callback functions
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
String _message = "Waiting for message...";
// Callback to receive message from child component
void _onMessageReceived(String message) {
setState(() {
_message = message;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("Received message: $_message"),
ChildWidget(
// Parent passes data to child
initialMessage: "Hello, child component",
// Parent provides callback to child
onSendMessage: _onMessageReceived,
),
],
);
}
}
class ChildWidget extends StatelessWidget {
final String initialMessage;
final ValueChanged<String> onSendMessage;
const ChildWidget({
super.key,
required this.initialMessage,
required this.onSendMessage,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("Received initial message: $initialMessage"),
ElevatedButton(
onPressed: () {
// Child sends message to parent through callback
onSendMessage("Hello, parent component! This is the child component");
},
child: const Text("Send message to parent"),
),
],
);
}
}
2. Cross-Level Component Communication
In addition to InheritedWidget, you can use an event bus for cross-level component communication:
// Event bus implementation
class EventBus {
// Singleton pattern
static final EventBus _instance = EventBus._internal();
factory EventBus() => _instance;
EventBus._internal();
// Store event subscribers
final Map<Type, List<Function>> _eventListeners = {};
// Subscribe to events
void on<T>(void Function(T) listener) {
if (!_eventListeners.containsKey(T)) {
_eventListeners[T] = [];
}
_eventListeners[T]!.add(listener);
}
// Unsubscribe from events
void off<T>(void Function(T) listener) {
if (_eventListeners.containsKey(T)) {
_eventListeners[T]!.remove(listener);
if (_eventListeners[T]!.isEmpty) {
_eventListeners.remove(T);
}
}
}
// Send event
void emit<T>(T event) {
if (_eventListeners.containsKey(T)) {
// Make a copy of the list before iterating to avoid modification during iteration
List<Function> listeners = List.from(_eventListeners[T]!);
for (var listener in listeners) {
listener(event);
}
}
}
}
// Define event types
class UserLoginEvent {
final String username;
UserLoginEvent(this.username);
}
class UserLogoutEvent {
UserLogoutEvent();
}
// Component that sends events
class LoginButton extends StatelessWidget {
const LoginButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// Send login event
EventBus().emit(UserLoginEvent("John Doe"));
},
child: const Text("Login"),
);
}
}
// Component that receives events
class UserStatusWidget extends StatefulWidget {
const UserStatusWidget({super.key});
@override
State<UserStatusWidget> createState() => _UserStatusWidgetState();
}
class _UserStatusWidgetState extends State<UserStatusWidget> {
String _status = "Not logged in";
@override
void initState() {
super.initState();
// Subscribe to login events
EventBus().on<UserLoginEvent>((event) {
setState(() {
_status = "Logged in: ${event.username}";
});
});
// Subscribe to logout events
EventBus().on<UserLogoutEvent>((event) {
setState(() {
_status = "Not logged in";
});
});
}
@override
void dispose() {
// Unsubscribe to prevent memory leaks
EventBus().off<UserLoginEvent>((event) {});
EventBus().off<UserLogoutEvent>((event) {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text("User status: $_status");
}
}
3. Global State Management
For large applications, specialized state management solutions are recommended, such as:
- Provider
- Bloc/Cubit
- Riverpod
- GetX
These solutions build on InheritedWidget to provide more comprehensive state management capabilities, including state change notifications, dependency injection, and lifecycle management.
VII. Component Encapsulation Practice: Form Component Library
Next, we'll comprehensively apply the knowledge learned in this lesson to encapsulate a practical form component library, including input fields, selectors, form validation, and other features.
import 'package:flutter/material.dart';
// 1. Form field model
class FormFieldData {
final String id;
dynamic value;
String? errorMessage;
bool touched;
FormFieldData({
required this.id,
this.value,
this.errorMessage,
this.touched = false,
});
}
// 2. Form state management (using InheritedWidget)
class FormProvider extends InheritedWidget {
final Map<String, FormFieldData> _fields = {};
final void Function() onFormChanged;
FormProvider({
super.key,
required super.child,
required this.onFormChanged,
});
static FormProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<FormProvider>();
}
// Get field value
dynamic getValue(String id) {
return _fields[id]?.value;
}
// Set field value
void setValue(String id, dynamic value, {bool validate = true}) {
if (!_fields.containsKey(id)) {
_fields[id] = FormFieldData(id: id);
}
_fields[id]!.value = value;
_fields[id]!.touched = true;
if (validate) {
validateField(id);
}
onFormChanged();
}
// Validate field
bool validateField(String id) {
// In real applications, there would be more complex validation logic
// Simplified here
final field = _fields[id];
if (field == null) return true;
if (field.value == null || field.value.toString().isEmpty) {
field.errorMessage = "This field cannot be empty";
return false;
}
field.errorMessage = null;
return true;
}
// Validate entire form
bool validate() {
bool isValid = true;
for (var field in _fields.values) {
field.touched = true;
if (!validateField(field.id)) {
isValid = false;
}
}
onFormChanged();
return isValid;
}
// Get field error message
String? getError(String id) {
return _fields[id]?.errorMessage;
}
// Check if field has been touched
bool isTouched(String id) {
return _fields[id]?.touched ?? false;
}
@override
bool updateShouldNotify(FormProvider oldWidget) {
return true;
}
}
// 3. Form container component
class FormContainer extends StatefulWidget {
final Widget child;
final void Function(bool isValid) onValidationChanged;
final void Function(Map<String, dynamic> values) onSubmit;
const FormContainer({
super.key,
required this.child,
required this.onValidationChanged,
required this.onSubmit,
});
@override
State<FormContainer> createState() => _FormContainerState();
}
class _FormContainerState extends State<FormContainer> {
bool _isValid = false;
void _onFormChanged(BuildContext context) {
final formProvider = FormProvider.of(context);
if (formProvider != null) {
final isValid = formProvider.validate();
setState(() {
_isValid = isValid;
});
widget.onValidationChanged(isValid);
}
}
void _handleSubmit(BuildContext context) {
final formProvider = FormProvider.of(context);
if (formProvider != null && formProvider.validate()) {
// Collect form data
final values = <String, dynamic>{};
formProvider._fields.forEach((key, value) {
values[key] = value.value;
});
widget.onSubmit(values);
}
}
@override
Widget build(BuildContext context) {
return FormProvider(
onFormChanged: () => _onFormChanged(context),
child: Column(
children: [
widget.child,
const SizedBox(height: 20),
LoadingButton(
text: "Submit",
onPressed: _isValid ? () => _handleSubmit(context) : null,
),
],
),
);
}
}
// 4. Custom input field component
class FormInputField extends StatelessWidget {
final String id;
final String label;
final String hintText;
final TextInputType keyboardType;
final bool obscureText;
final FormFieldValidator<String>? validator;
const FormInputField({
super.key,
required this.id,
required this.label,
this.hintText = "",
this.keyboardType = TextInputType.text,
this.obscureText = false,
this.validator,
});
@override
Widget build(BuildContext context) {
final formProvider = FormProvider.of(context);
if (formProvider == null) {
return const SizedBox();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
TextFormField(
initialValue: formProvider.getValue(id)?.toString() ?? "",
keyboardType: keyboardType,
obscureText: obscureText,
decoration: InputDecoration(
hintText: hintText,
border: const OutlineInputBorder(),
errorText: formProvider.isTouched(id) ? formProvider.getError(id) : null,
),
onChanged: (value) {
formProvider.setValue(id, value);
},
),
const SizedBox(height: 16),
],
);
}
}
// 5. Using the form component library
class RegisterFormDemo extends StatelessWidget {
const RegisterFormDemo({super.key});
void _onValidationChanged(bool isValid) {
print("Form validation status: ${isValid ? "valid" : "invalid"}");
}
void _onSubmit(Map<String, dynamic> values) {
print("Form submitted data: $values");
// Form submission logic such as network requests can be handled here
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Registration Form')),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: FormContainer(
onValidationChanged: _onValidationChanged,
onSubmit: _onSubmit,
child: Column(
children: [
FormInputField(
id: "username",
label: "Username",
hintText: "Please enter your username",
),
FormInputField(
id: "email",
label: "Email",
hintText: "Please enter your email",
keyboardType: TextInputType.emailAddress,
),
FormInputField(
id: "password",
label: "Password",
hintText: "Please enter your password",
obscureText: true,
),
],
),
),
),
);
}
}
Top comments (0)