flutter学习第 15 节:自定义 Widget 与组件封装

在 Flutter 开发中,随着应用规模的扩大,合理封装自定义 Widget 和可复用组件变得至关重要。良好的组件设计可以显著提高代码复用率、降低维护成本,并保证 UI 风格的一致性。本节课将深入探讨自定义 Widget 的设计原则、实现方法以及组件间的数据传递机制。

一、封装可复用组件的原则

设计高质量的可复用组件需要遵循一些核心原则,这些原则有助于创建出灵活、健壮且易于维护的组件:

1. 单一职责原则

每个组件应专注于完成单一功能,避免设计 "万能组件"。例如,一个按钮组件不应同时处理表单验证逻辑,一个列表组件不应包含具体的列表项渲染逻辑。
反例:一个既负责网络请求又负责 UI 展示的组件
正例:将网络请求与 UI 展示分离,UI 组件只负责展示数据

2. 高内聚低耦合

  • 高内聚:组件内部相关功能应紧密结合,形成一个有机整体
  • 低耦合:组件之间应通过明确定义的接口进行通信,减少直接依赖

3. 配置灵活性

通过参数配置让组件适应不同场景,但需平衡灵活性与复杂性:

  • 提供合理的默认值,减少使用成本
  • 关键属性应可配置,次要属性可固定
  • 使用 boolenum 等类型限制配置选项,避免错误使用

4. 可扩展性

设计时预留扩展点,便于未来功能扩展:

  • 通过 childbuilder 参数允许自定义子组件
  • 使用继承或组合方式扩展基础组件功能
  • 避免硬编码业务逻辑

5. 一致性与可识别性

  • 保持组件风格与应用整体设计一致
  • 组件行为应符合用户预期(如按钮点击反馈)
  • 相似功能的组件应保持 API 设计的一致性

6. 可测试性

  • 组件应易于实例化和测试
  • 避免在组件内部创建全局状态
  • 关键逻辑应可独立测试


二、自定义 Widget 基础

Flutter 中自定义组件主要有两种方式:组合现有 Widget自定义 RenderObject。对于大多数场景,我们应优先选择组合方式,因为它更简单且能满足大部分需求。

1. StatelessWidget 与 StatefulWidget 的选择

  • StatelessWidget:适用于无状态或状态由父组件管理的场景,如静态展示组件、纯 UI 组件
  • StatefulWidget:适用于有内部状态管理的组件,如计数器、展开 / 折叠面板

选择建议:尽量使用 StatelessWidget,将状态提升到父组件或状态管理框架中,可使组件更易于测试和复用。

2. 组件命名规范

  • 使用 PascalCase(帕斯卡命名法),如 PrimaryButtonUserProfileCard
  • 名称应准确描述组件功能,避免过于抽象或笼统
  • 对于具有相似基础功能但样式不同的组件,可使用一致的前缀,如 PrimaryButtonSecondaryButton


三、自定义 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. 高级参数校验

对于复杂的参数校验逻辑,可以在 initStatebuild 方法中进行:

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,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容