在 Flutter 开发中,随着应用规模的扩大,合理封装自定义 Widget 和可复用组件变得至关重要。良好的组件设计可以显著提高代码复用率、降低维护成本,并保证 UI 风格的一致性。本节课将深入探讨自定义 Widget 的设计原则、实现方法以及组件间的数据传递机制。
一、封装可复用组件的原则
设计高质量的可复用组件需要遵循一些核心原则,这些原则有助于创建出灵活、健壮且易于维护的组件:
1. 单一职责原则
每个组件应专注于完成单一功能,避免设计 "万能组件"。例如,一个按钮组件不应同时处理表单验证逻辑,一个列表组件不应包含具体的列表项渲染逻辑。
反例:一个既负责网络请求又负责 UI 展示的组件
正例:将网络请求与 UI 展示分离,UI 组件只负责展示数据
2. 高内聚低耦合
- 高内聚:组件内部相关功能应紧密结合,形成一个有机整体
- 低耦合:组件之间应通过明确定义的接口进行通信,减少直接依赖
3. 配置灵活性
通过参数配置让组件适应不同场景,但需平衡灵活性与复杂性:
- 提供合理的默认值,减少使用成本
- 关键属性应可配置,次要属性可固定
- 使用
bool、enum等类型限制配置选项,避免错误使用
4. 可扩展性
设计时预留扩展点,便于未来功能扩展:
- 通过
child或builder参数允许自定义子组件 - 使用继承或组合方式扩展基础组件功能
- 避免硬编码业务逻辑
5. 一致性与可识别性
- 保持组件风格与应用整体设计一致
- 组件行为应符合用户预期(如按钮点击反馈)
- 相似功能的组件应保持 API 设计的一致性
6. 可测试性
- 组件应易于实例化和测试
- 避免在组件内部创建全局状态
- 关键逻辑应可独立测试
二、自定义 Widget 基础
Flutter 中自定义组件主要有两种方式:组合现有 Widget 和 自定义 RenderObject。对于大多数场景,我们应优先选择组合方式,因为它更简单且能满足大部分需求。
1. StatelessWidget 与 StatefulWidget 的选择
- StatelessWidget:适用于无状态或状态由父组件管理的场景,如静态展示组件、纯 UI 组件
- StatefulWidget:适用于有内部状态管理的组件,如计数器、展开 / 折叠面板
选择建议:尽量使用 StatelessWidget,将状态提升到父组件或状态管理框架中,可使组件更易于测试和复用。
2. 组件命名规范
- 使用 PascalCase(帕斯卡命名法),如
PrimaryButton、UserProfileCard - 名称应准确描述组件功能,避免过于抽象或笼统
- 对于具有相似基础功能但样式不同的组件,可使用一致的前缀,如
PrimaryButton、SecondaryButton
三、自定义 Widget 示例
1. 示例一:带加载状态的按钮(StatefulWidget)
这个按钮组件将支持加载状态、禁用状态、自定义样式等功能,适用于表单提交、数据请求等场景。
import 'package:flutter/material.dart';
/// 带加载状态的按钮组件
/// 支持正常、加载、禁用三种状态
/// 可自定义颜色、圆角、文本样式等
class LoadingButton extends StatefulWidget {
/// 按钮文本
final String text;
/// 点击回调
final VoidCallback? onPressed;
/// 是否处于加载状态
final bool isLoading;
/// 按钮主色调
final Color? color;
/// 文本颜色
final Color textColor;
/// 禁用状态颜色
final Color disabledColor;
/// 禁用状态文本颜色
final Color disabledTextColor;
/// 按钮圆角
final double borderRadius;
/// 按钮内边距
final EdgeInsetsGeometry padding;
/// 加载指示器颜色
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, "按钮文本不能为空");
@override
State<LoadingButton> createState() => _LoadingButtonState();
}
class _LoadingButtonState extends State<LoadingButton> {
// 按钮是否可点击
bool get _isEnabled => widget.onPressed != null && !widget.isLoading;
@override
Widget build(BuildContext context) {
// 获取主题中的主色调作为默认颜色
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),
);
}
// 根据状态获取按钮颜色
Color _getButtonColor(Color defaultColor) {
if (widget.isLoading) {
return defaultColor.withOpacity(0.8);
}
return defaultColor;
}
// 构建按钮内容(文本或加载指示器)
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(
"加载中...",
style: TextStyle(
color: _isEnabled ? widget.textColor : widget.disabledTextColor,
),
),
],
);
}
return Text(
widget.text,
style: TextStyle(
color: _isEnabled ? widget.textColor : widget.disabledTextColor,
),
);
}
}
// 使用示例
class LoadingButtonDemo extends StatefulWidget {
const LoadingButtonDemo({super.key});
@override
State<LoadingButtonDemo> createState() => _LoadingButtonDemoState();
}
class _LoadingButtonDemoState extends State<LoadingButtonDemo> {
bool _isLoading = false;
void _handlePress() async {
// 模拟网络请求
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: "提交",
onPressed: _handlePress,
isLoading: _isLoading,
),
const SizedBox(height: 20),
LoadingButton(
text: "禁用状态",
onPressed: null, // onPressed为null时按钮禁用
color: Colors.grey,
),
const SizedBox(height: 20),
LoadingButton(
text: "自定义样式",
onPressed: () {},
color: Colors.purple,
borderRadius: 20,
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
),
],
),
),
);
}
}
2. 示例二:通用标题栏(StatelessWidget)
实现一个可高度定制的标题栏组件,支持左右按钮、标题样式自定义、背景色设置等功能。
import 'package:flutter/material.dart';
/// 通用标题栏组件
/// 支持自定义标题、左右按钮、背景色等
class CommonAppBar extends StatelessWidget implements PreferredSizeWidget {
/// 标题
final String title;
/// 标题组件,优先级高于title
final Widget? titleWidget;
/// 左侧按钮
final Widget? leading;
/// 右侧按钮列表
final List<Widget>? actions;
/// 背景色
final Color? backgroundColor;
/// 标题样式
final TextStyle? titleStyle;
/// 阴影高度
final double elevation;
/// 标题是否居中
final bool centerTitle;
/// 左侧按钮点击回调(仅当未自定义leading时有效)
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,
);
}
// 构建默认左侧按钮(返回按钮)
Widget? _buildDefaultLeading(BuildContext context) {
// 如果是导航栈的第一个页面,不显示返回按钮
if (Navigator.canPop(context)) {
return IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: onLeadingPressed ?? () => Navigator.pop(context),
);
}
return null;
}
// 定义标题栏高度,使用默认的56.0
@override
Size get preferredSize => const Size.fromHeight(56.0);
}
// 使用示例
class CommonAppBarDemo extends StatelessWidget {
const CommonAppBarDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: CommonAppBar(
title: "首页",
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () => print("搜索"),
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () => print("更多"),
),
],
),
body: const Center(
child: Text("页面内容"),
),
);
}
}
// 自定义标题样式示例
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("自定义标题"),
],
),
backgroundColor: Colors.purple,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () => print("菜单"),
),
),
body: const Center(
child: Text("带自定义标题的页面"),
),
);
}
}
四、组件参数校验与默认值设置
良好的参数设计是组件易用性的关键,合理的默认值可以减少使用成本,而严格的参数校验可以提前发现错误。
1. 必填参数与可选参数
使用 required 关键字标记必填参数,让编译器帮助我们检查参数是否完整:
class CustomButton extends StatelessWidget {
// 必填参数
final String text;
final VoidCallback onPressed;
// 可选参数
final Color? color;
// 使用required标记必填参数
const CustomButton({
super.key,
required this.text,
required this.onPressed,
this.color,
});
// ...
}
2. 默认值设置
为可选参数提供合理的默认值,降低组件使用复杂度:
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,
// 提供默认值
this.color = Colors.blue,
this.borderRadius = 8.0,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
});
// ...
}
3. 参数校验
使用 assert 语句在开发阶段进行参数校验,提前发现错误:
class Avatar extends StatelessWidget {
final String url;
final double radius;
const Avatar({
super.key,
required this.url,
this.radius = 24.0,
}) :
// 校验radius必须为正数
assert(radius > 0, "radius必须大于0"),
// 校验url不为空
assert(url.isNotEmpty, "url不能为空");
@override
Widget build(BuildContext context) {
return ClipOval(
child: Image.network(
url,
width: radius * 2,
height: radius * 2,
fit: BoxFit.cover,
),
);
}
}
4. 高级参数校验
对于复杂的参数校验逻辑,可以在 initState 或 build 方法中进行:
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();
}
// 复杂参数校验
void _validateParams() {
if (widget.min >= widget.max) {
throw FlutterError("min必须小于max: min=${widget.min}, max=${widget.max}");
}
if (widget.value < widget.min || widget.value > widget.max) {
throw FlutterError("value必须在[min, max]范围内: value=${widget.value}, min=${widget.min}, max=${widget.max}");
}
}
// ...
}
五、使用 InheritedWidget 实现数据跨层传递
在复杂应用中,组件嵌套层级可能很深,通过构造函数逐层传递数据会非常繁琐。InheritedWidget 提供了一种高效的跨层数据传递方式,允许子组件直接访问上层数据。
1. InheritedWidget 基础
InheritedWidget 是一种特殊的 Widget,它可以在 Widget 树中向下传递数据,子组件可以通过 BuildContext 访问这些数据。
// 1. 创建自定义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,
});
// 提供静态方法方便子组件获取
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
// 当数据变化时,是否通知依赖的子组件重建
@override
bool updateShouldNotify(AppTheme oldWidget) {
return data.primaryColor != oldWidget.data.primaryColor ||
data.secondaryColor != oldWidget.data.secondaryColor ||
data.textStyle != oldWidget.data.textStyle;
}
}
2. 在 Widget 树中使用
// 2. 在Widget树顶层提供数据
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// 创建主题数据
final themeData = ThemeData(
primaryColor: Colors.blue,
secondaryColor: Colors.green,
textStyle: const TextStyle(
color: Colors.black87,
fontSize: 16,
),
);
// 提供InheritedWidget
return AppTheme(
data: themeData,
child: const MaterialApp(
home: HomePage(),
),
);
}
}
// 3. 深层子组件访问数据
class DeepNestedWidget extends StatelessWidget {
const DeepNestedWidget({super.key});
@override
Widget build(BuildContext context) {
// 获取主题数据
final appTheme = AppTheme.of(context);
if (appTheme == null) {
return const Text("未找到主题数据");
}
return Container(
color: appTheme.data.secondaryColor,
padding: const EdgeInsets.all(16),
child: Text(
"使用主题样式的文本",
style: appTheme.data.textStyle,
),
);
}
}
// 中间组件(无需传递数据)
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("InheritedWidget示例")),
body: const Center(
child: IntermediateWidget(),
),
);
}
}
class IntermediateWidget extends StatelessWidget {
const IntermediateWidget({super.key});
@override
Widget build(BuildContext context) {
return const DeepNestedWidget();
}
}
3. 动态更新 InheritedWidget 数据
InheritedWidget 本身是不可变的,要实现数据动态更新,需要结合 StatefulWidget:
class ThemeProvider extends StatefulWidget {
final Widget child;
const ThemeProvider({
super.key,
required this.child,
});
// 提供静态方法获取状态
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();
// 初始化主题数据
_themeData = ThemeData(
primaryColor: Colors.blue,
secondaryColor: Colors.green,
textStyle: const TextStyle(
color: Colors.black87,
fontSize: 16,
),
);
}
// 切换主题的方法
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,
);
}
}
// 使用动态主题
class ThemedButton extends StatelessWidget {
const ThemedButton({super.key});
@override
Widget build(BuildContext context) {
final appTheme = AppTheme.of(context);
return ElevatedButton(
onPressed: () {
// 切换主题
ThemeProvider.of(context).toggleTheme();
},
style: ElevatedButton.styleFrom(
backgroundColor: appTheme?.data.primaryColor,
),
child: Text(
"切换主题",
style: TextStyle(
color: appTheme?.data.textStyle.color,
),
),
);
}
}
4. InheritedWidget 注意事项
-
性能考量:
updateShouldNotify方法应准确判断数据是否真的发生变化,避免不必要的重建 - 不要过度使用:简单场景下,通过构造函数传递数据更直接
-
数据类型:
InheritedWidget适合传递全局配置、主题、用户信息等跨组件共享的数据 -
依赖管理:使用
dependOnInheritedWidgetOfExactType会建立依赖关系,数据变化时会触发重建;使用getInheritedWidgetOfExactType则不会建立依赖关系
六、组件通信方式
在复杂应用中,组件之间需要进行通信,Flutter 提供了多种组件通信方式:
1. 父子组件通信
- 父传子:通过构造函数参数传递
- 子传父:通过回调函数传递
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
String _message = "等待消息...";
// 接收子组件消息的回调
void _onMessageReceived(String message) {
setState(() {
_message = message;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("收到的消息: $_message"),
ChildWidget(
// 父组件向子组件传递数据
initialMessage: "你好,子组件",
// 父组件提供回调给子组件
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("收到的初始消息: $initialMessage"),
ElevatedButton(
onPressed: () {
// 子组件通过回调向父组件发送消息
onSendMessage("你好,父组件!我是子组件");
},
child: const Text("发送消息给父组件"),
),
],
);
}
}
2. 跨级组件通信
除了 InheritedWidget,还可以使用事件总线实现跨级组件通信:
// 事件总线实现
class EventBus {
// 单例模式
static final EventBus _instance = EventBus._internal();
factory EventBus() => _instance;
EventBus._internal();
// 存储事件订阅者
final Map<Type, List<Function>> _eventListeners = {};
// 订阅事件
void on<T>(void Function(T) listener) {
if (!_eventListeners.containsKey(T)) {
_eventListeners[T] = [];
}
_eventListeners[T]!.add(listener);
}
// 取消订阅
void off<T>(void Function(T) listener) {
if (_eventListeners.containsKey(T)) {
_eventListeners[T]!.remove(listener);
if (_eventListeners[T]!.isEmpty) {
_eventListeners.remove(T);
}
}
}
// 发送事件
void emit<T>(T event) {
if (_eventListeners.containsKey(T)) {
// 复制一份列表再遍历,避免在遍历中修改列表
List<Function> listeners = List.from(_eventListeners[T]!);
for (var listener in listeners) {
listener(event);
}
}
}
}
// 定义事件类型
class UserLoginEvent {
final String username;
UserLoginEvent(this.username);
}
class UserLogoutEvent {
UserLogoutEvent();
}
// 发送事件的组件
class LoginButton extends StatelessWidget {
const LoginButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// 发送登录事件
EventBus().emit(UserLoginEvent("张三"));
},
child: const Text("登录"),
);
}
}
// 接收事件的组件
class UserStatusWidget extends StatefulWidget {
const UserStatusWidget({super.key});
@override
State<UserStatusWidget> createState() => _UserStatusWidgetState();
}
class _UserStatusWidgetState extends State<UserStatusWidget> {
String _status = "未登录";
@override
void initState() {
super.initState();
// 订阅登录事件
EventBus().on<UserLoginEvent>((event) {
setState(() {
_status = "已登录:${event.username}";
});
});
// 订阅登出事件
EventBus().on<UserLogoutEvent>((event) {
setState(() {
_status = "未登录";
});
});
}
@override
void dispose() {
// 取消订阅,避免内存泄漏
EventBus().off<UserLoginEvent>((event) {});
EventBus().off<UserLogoutEvent>((event) {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text("用户状态:$_status");
}
}
3. 全局状态管理
对于大型应用,推荐使用专门的状态管理方案,如:
- Provider
- Bloc/Cubit
- Riverpod
- GetX
这些方案在 InheritedWidget 基础上提供了更完善的状态管理能力,包括状态变更通知、依赖注入、生命周期管理等。
七、组件封装实战:表单组件库
下面我们将综合运用本节课所学知识,封装一套实用的表单组件库,包括输入框、选择器、表单验证等功能。
import 'package:flutter/material.dart';
// 1. 表单字段模型
class FormFieldData {
final String id;
dynamic value;
String? errorMessage;
bool touched;
FormFieldData({
required this.id,
this.value,
this.errorMessage,
this.touched = false,
});
}
// 2. 表单状态管理(使用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>();
}
// 获取字段值
dynamic getValue(String id) {
return _fields[id]?.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();
}
// 验证字段
bool validateField(String id) {
// 实际应用中会有更复杂的验证逻辑
// 这里简化处理
final field = _fields[id];
if (field == null) return true;
if (field.value == null || field.value.toString().isEmpty) {
field.errorMessage = "此字段不能为空";
return false;
}
field.errorMessage = null;
return true;
}
// 验证整个表单
bool validate() {
bool isValid = true;
for (var field in _fields.values) {
field.touched = true;
if (!validateField(field.id)) {
isValid = false;
}
}
onFormChanged();
return isValid;
}
// 获取字段错误信息
String? getError(String id) {
return _fields[id]?.errorMessage;
}
// 检查字段是否被触摸过
bool isTouched(String id) {
return _fields[id]?.touched ?? false;
}
@override
bool updateShouldNotify(FormProvider oldWidget) {
return true;
}
}
// 3. 表单组件
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()) {
// 收集表单数据
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: "提交",
onPressed: _isValid ? () => _handleSubmit(context) : null,
),
],
),
);
}
}
// 4. 自定义输入框组件
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. 使用表单组件库
class RegisterFormDemo extends StatelessWidget {
const RegisterFormDemo({super.key});
void _onValidationChanged(bool isValid) {
print("表单验证状态: ${isValid ? "有效" : "无效"}");
}
void _onSubmit(Map<String, dynamic> values) {
print("表单提交数据: $values");
// 这里可以处理表单提交逻辑,如网络请求等
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('注册表单')),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: FormContainer(
onValidationChanged: _onValidationChanged,
onSubmit: _onSubmit,
child: Column(
children: [
FormInputField(
id: "username",
label: "用户名",
hintText: "请输入用户名",
),
FormInputField(
id: "email",
label: "邮箱",
hintText: "请输入邮箱",
keyboardType: TextInputType.emailAddress,
),
FormInputField(
id: "password",
label: "密码",
hintText: "请输入密码",
obscureText: true,
),
],
),
),
),
);
}
}