在 Flutter 開發(fā)中,隨著應用規(guī)模的擴大,合理封裝自定義 Widget 和可復用組件變得至關(guān)重要。良好的組件設(shè)計可以顯著提高代碼復用率、降低維護成本,并保證 UI 風格的一致性。本節(jié)課將深入探討自定義 Widget 的設(shè)計原則、實現(xiàn)方法以及組件間的數(shù)據(jù)傳遞機制。
一、封裝可復用組件的原則
設(shè)計高質(zhì)量的可復用組件需要遵循一些核心原則,這些原則有助于創(chuàng)建出靈活、健壯且易于維護的組件:
1. 單一職責原則
每個組件應專注于完成單一功能,避免設(shè)計 "萬能組件"。例如,一個按鈕組件不應同時處理表單驗證邏輯,一個列表組件不應包含具體的列表項渲染邏輯。
反例:一個既負責網(wǎng)絡(luò)請求又負責 UI 展示的組件
正例:將網(wǎng)絡(luò)請求與 UI 展示分離,UI 組件只負責展示數(shù)據(jù)
2. 高內(nèi)聚低耦合
- 高內(nèi)聚:組件內(nèi)部相關(guān)功能應緊密結(jié)合,形成一個有機整體
- 低耦合:組件之間應通過明確定義的接口進行通信,減少直接依賴
3. 配置靈活性
通過參數(shù)配置讓組件適應不同場景,但需平衡靈活性與復雜性:
- 提供合理的默認值,減少使用成本
- 關(guān)鍵屬性應可配置,次要屬性可固定
- 使用
bool、enum等類型限制配置選項,避免錯誤使用
4. 可擴展性
設(shè)計時預留擴展點,便于未來功能擴展:
- 通過
child或builder參數(shù)允許自定義子組件 - 使用繼承或組合方式擴展基礎(chǔ)組件功能
- 避免硬編碼業(yè)務(wù)邏輯
5. 一致性與可識別性
- 保持組件風格與應用整體設(shè)計一致
- 組件行為應符合用戶預期(如按鈕點擊反饋)
- 相似功能的組件應保持 API 設(shè)計的一致性
6. 可測試性
- 組件應易于實例化和測試
- 避免在組件內(nèi)部創(chuàng)建全局狀態(tài)
- 關(guān)鍵邏輯應可獨立測試
二、自定義 Widget 基礎(chǔ)
Flutter 中自定義組件主要有兩種方式:組合現(xiàn)有 Widget 和 自定義 RenderObject。對于大多數(shù)場景,我們應優(yōu)先選擇組合方式,因為它更簡單且能滿足大部分需求。
1. StatelessWidget 與 StatefulWidget 的選擇
- StatelessWidget:適用于無狀態(tài)或狀態(tài)由父組件管理的場景,如靜態(tài)展示組件、純 UI 組件
- StatefulWidget:適用于有內(nèi)部狀態(tài)管理的組件,如計數(shù)器、展開 / 折疊面板
選擇建議:盡量使用 StatelessWidget,將狀態(tài)提升到父組件或狀態(tài)管理框架中,可使組件更易于測試和復用。
2. 組件命名規(guī)范
- 使用 PascalCase(帕斯卡命名法),如
PrimaryButton、UserProfileCard - 名稱應準確描述組件功能,避免過于抽象或籠統(tǒng)
- 對于具有相似基礎(chǔ)功能但樣式不同的組件,可使用一致的前綴,如
PrimaryButton、SecondaryButton
三、自定義 Widget 示例
1. 示例一:帶加載狀態(tài)的按鈕(StatefulWidget)
這個按鈕組件將支持加載狀態(tài)、禁用狀態(tài)、自定義樣式等功能,適用于表單提交、數(shù)據(jù)請求等場景。
import 'package:flutter/material.dart';
/// 帶加載狀態(tài)的按鈕組件
/// 支持正常、加載、禁用三種狀態(tài)
/// 可自定義顏色、圓角、文本樣式等
class LoadingButton extends StatefulWidget {
/// 按鈕文本
final String text;
/// 點擊回調(diào)
final VoidCallback? onPressed;
/// 是否處于加載狀態(tài)
final bool isLoading;
/// 按鈕主色調(diào)
final Color? color;
/// 文本顏色
final Color textColor;
/// 禁用狀態(tài)顏色
final Color disabledColor;
/// 禁用狀態(tài)文本顏色
final Color disabledTextColor;
/// 按鈕圓角
final double borderRadius;
/// 按鈕內(nèi)邊距
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) {
// 獲取主題中的主色調(diào)作為默認顏色
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),
);
}
// 根據(jù)狀態(tài)獲取按鈕顏色
Color _getButtonColor(Color defaultColor) {
if (widget.isLoading) {
return defaultColor.withOpacity(0.8);
}
return defaultColor;
}
// 構(gòu)建按鈕內(nèi)容(文本或加載指示器)
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 {
// 模擬網(wǎng)絡(luò)請求
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: "禁用狀態(tài)",
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)
實現(xiàn)一個可高度定制的標題欄組件,支持左右按鈕、標題樣式自定義、背景色設(shè)置等功能。
import 'package:flutter/material.dart';
/// 通用標題欄組件
/// 支持自定義標題、左右按鈕、背景色等
class CommonAppBar extends StatelessWidget implements PreferredSizeWidget {
/// 標題
final String title;
/// 標題組件,優(yōu)先級高于title
final Widget? titleWidget;
/// 左側(cè)按鈕
final Widget? leading;
/// 右側(cè)按鈕列表
final List<Widget>? actions;
/// 背景色
final Color? backgroundColor;
/// 標題樣式
final TextStyle? titleStyle;
/// 陰影高度
final double elevation;
/// 標題是否居中
final bool centerTitle;
/// 左側(cè)按鈕點擊回調(diào)(僅當未自定義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,
);
}
// 構(gòu)建默認左側(cè)按鈕(返回按鈕)
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("頁面內(nèi)容"),
),
);
}
}
// 自定義標題樣式示例
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("帶自定義標題的頁面"),
),
);
}
}
四、組件參數(shù)校驗與默認值設(shè)置
良好的參數(shù)設(shè)計是組件易用性的關(guān)鍵,合理的默認值可以減少使用成本,而嚴格的參數(shù)校驗可以提前發(fā)現(xiàn)錯誤。
1. 必填參數(shù)與可選參數(shù)
使用 required 關(guān)鍵字標記必填參數(shù),讓編譯器幫助我們檢查參數(shù)是否完整:
class CustomButton extends StatelessWidget {
// 必填參數(shù)
final String text;
final VoidCallback onPressed;
// 可選參數(shù)
final Color? color;
// 使用required標記必填參數(shù)
const CustomButton({
super.key,
required this.text,
required this.onPressed,
this.color,
});
// ...
}
2. 默認值設(shè)置
為可選參數(shù)提供合理的默認值,降低組件使用復雜度:
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. 參數(shù)校驗
使用 assert 語句在開發(fā)階段進行參數(shù)校驗,提前發(fā)現(xiàn)錯誤:
class Avatar extends StatelessWidget {
final String url;
final double radius;
const Avatar({
super.key,
required this.url,
this.radius = 24.0,
}) :
// 校驗radius必須為正數(shù)
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. 高級參數(shù)校驗
對于復雜的參數(shù)校驗邏輯,可以在 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();
}
// 復雜參數(shù)校驗
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]范圍內(nèi): value=${widget.value}, min=${widget.min}, max=${widget.max}");
}
}
// ...
}
五、使用 InheritedWidget 實現(xiàn)數(shù)據(jù)跨層傳遞
在復雜應用中,組件嵌套層級可能很深,通過構(gòu)造函數(shù)逐層傳遞數(shù)據(jù)會非常繁瑣。InheritedWidget 提供了一種高效的跨層數(shù)據(jù)傳遞方式,允許子組件直接訪問上層數(shù)據(jù)。
1. InheritedWidget 基礎(chǔ)
InheritedWidget 是一種特殊的 Widget,它可以在 Widget 樹中向下傳遞數(shù)據(jù),子組件可以通過 BuildContext 訪問這些數(shù)據(jù)。
// 1. 創(chuàng)建自定義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,
});
// 提供靜態(tài)方法方便子組件獲取
static AppTheme? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppTheme>();
}
// 當數(shù)據(jù)變化時,是否通知依賴的子組件重建
@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樹頂層提供數(shù)據(jù)
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
// 創(chuàng)建主題數(shù)據(jù)
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. 深層子組件訪問數(shù)據(jù)
class DeepNestedWidget extends StatelessWidget {
const DeepNestedWidget({super.key});
@override
Widget build(BuildContext context) {
// 獲取主題數(shù)據(jù)
final appTheme = AppTheme.of(context);
if (appTheme == null) {
return const Text("未找到主題數(shù)據(jù)");
}
return Container(
color: appTheme.data.secondaryColor,
padding: const EdgeInsets.all(16),
child: Text(
"使用主題樣式的文本",
style: appTheme.data.textStyle,
),
);
}
}
// 中間組件(無需傳遞數(shù)據(jù))
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. 動態(tài)更新 InheritedWidget 數(shù)據(jù)
InheritedWidget 本身是不可變的,要實現(xiàn)數(shù)據(jù)動態(tài)更新,需要結(jié)合 StatefulWidget:
class ThemeProvider extends StatefulWidget {
final Widget child;
const ThemeProvider({
super.key,
required this.child,
});
// 提供靜態(tài)方法獲取狀態(tài)
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();
// 初始化主題數(shù)據(jù)
_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,
);
}
}
// 使用動態(tài)主題
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方法應準確判斷數(shù)據(jù)是否真的發(fā)生變化,避免不必要的重建 - 不要過度使用:簡單場景下,通過構(gòu)造函數(shù)傳遞數(shù)據(jù)更直接
-
數(shù)據(jù)類型:
InheritedWidget適合傳遞全局配置、主題、用戶信息等跨組件共享的數(shù)據(jù) -
依賴管理:使用
dependOnInheritedWidgetOfExactType會建立依賴關(guān)系,數(shù)據(jù)變化時會觸發(fā)重建;使用getInheritedWidgetOfExactType則不會建立依賴關(guān)系
六、組件通信方式
在復雜應用中,組件之間需要進行通信,F(xiàn)lutter 提供了多種組件通信方式:
1. 父子組件通信
- 父傳子:通過構(gòu)造函數(shù)參數(shù)傳遞
- 子傳父:通過回調(diào)函數(shù)傳遞
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
String _message = "等待消息...";
// 接收子組件消息的回調(diào)
void _onMessageReceived(String message) {
setState(() {
_message = message;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("收到的消息: $_message"),
ChildWidget(
// 父組件向子組件傳遞數(shù)據(jù)
initialMessage: "你好,子組件",
// 父組件提供回調(diào)給子組件
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: () {
// 子組件通過回調(diào)向父組件發(fā)送消息
onSendMessage("你好,父組件!我是子組件");
},
child: const Text("發(fā)送消息給父組件"),
),
],
);
}
}
2. 跨級組件通信
除了 InheritedWidget,還可以使用事件總線實現(xiàn)跨級組件通信:
// 事件總線實現(xiàn)
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);
}
}
}
// 發(fā)送事件
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();
}
// 發(fā)送事件的組件
class LoginButton extends StatelessWidget {
const LoginButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// 發(fā)送登錄事件
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() {
// 取消訂閱,避免內(nèi)存泄漏
EventBus().off<UserLoginEvent>((event) {});
EventBus().off<UserLogoutEvent>((event) {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text("用戶狀態(tài):$_status");
}
}
3. 全局狀態(tài)管理
對于大型應用,推薦使用專門的狀態(tài)管理方案,如:
- Provider
- Bloc/Cubit
- Riverpod
- GetX
這些方案在 InheritedWidget 基礎(chǔ)上提供了更完善的狀態(tài)管理能力,包括狀態(tài)變更通知、依賴注入、生命周期管理等。
七、組件封裝實戰(zhàn):表單組件庫
下面我們將綜合運用本節(jié)課所學知識,封裝一套實用的表單組件庫,包括輸入框、選擇器、表單驗證等功能。
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. 表單狀態(tài)管理(使用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;
}
// 設(shè)置字段值
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()) {
// 收集表單數(shù)據(jù)
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("表單驗證狀態(tài): ${isValid ? "有效" : "無效"}");
}
void _onSubmit(Map<String, dynamic> values) {
print("表單提交數(shù)據(jù): $values");
// 這里可以處理表單提交邏輯,如網(wǎng)絡(luò)請求等
}
@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,
),
],
),
),
),
);
}
}