Flutter 自定义组件:构建可复用的 UI 组件

张开发
2026/4/8 15:57:15 15 分钟阅读

分享文章

Flutter 自定义组件:构建可复用的 UI 组件
Flutter 自定义组件构建可复用的 UI 组件掌握 Flutter 自定义组件的高级技巧创建更加灵活、可复用的 UI 组件。一、自定义组件概述作为一名追求像素级还原的 UI 匠人我对 Flutter 自定义组件有着深入的研究。自定义组件是 Flutter 开发中的重要组成部分它允许我们创建符合特定需求的 UI 元素提高代码的可重用性和可维护性。从简单的按钮到复杂的表单控件自定义组件为我们提供了无限的创意空间。二、基础自定义组件1. 无状态组件import package:flutter/material.dart; class CustomButton extends StatelessWidget { final String text; final VoidCallback onPressed; final Color? backgroundColor; final Color? textColor; final double? borderRadius; final double? padding; const CustomButton({ Key? key, required this.text, required this.onPressed, this.backgroundColor, this.textColor, this.borderRadius, this.padding, }) : super(key: key); override Widget build(BuildContext context) { return ElevatedButton( onPressed: onPressed, style: ElevatedButton.styleFrom( backgroundColor: backgroundColor ?? Colors.blue, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(borderRadius ?? 8), ), padding: EdgeInsets.all(padding ?? 12), ), child: Text( text, style: TextStyle( color: textColor ?? Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ); } } // 使用 class CustomButtonExample extends StatelessWidget { override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(自定义按钮示例)), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CustomButton( text: 默认按钮, onPressed: () print(默认按钮被点击), ), SizedBox(height: 20), CustomButton( text: 红色按钮, onPressed: () print(红色按钮被点击), backgroundColor: Colors.red, borderRadius: 20, ), SizedBox(height: 20), CustomButton( text: 绿色按钮, onPressed: () print(绿色按钮被点击), backgroundColor: Colors.green, textColor: Colors.white, padding: 16, ), ], ), ), ); } }2. 有状态组件class CounterButton extends StatefulWidget { final String text; final int initialCount; final ValueChangedint? onCountChanged; const CounterButton({ Key? key, required this.text, this.initialCount 0, this.onCountChanged, }) : super(key: key); override _CounterButtonState createState() _CounterButtonState(); } class _CounterButtonState extends StateCounterButton { late int _count; override void initState() { super.initState(); _count widget.initialCount; } void _increment() { setState(() { _count; widget.onCountChanged?.call(_count); }); } override Widget build(BuildContext context) { return Column( children: [ ElevatedButton( onPressed: _increment, child: Text(${widget.text}: $_count), ), SizedBox(height: 10), Text(当前计数: $_count), ], ); } } // 使用 class CounterButtonExample extends StatelessWidget { override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(计数按钮示例)), body: Center( child: CounterButton( text: 点击计数, initialCount: 5, onCountChanged: (count) { print(计数变化: $count); }, ), ), ); } }三、高级自定义组件1. 复合组件class CardWithImage extends StatelessWidget { final String title; final String subtitle; final String imageUrl; final VoidCallback? onTap; const CardWithImage({ Key? key, required this.title, required this.subtitle, required this.imageUrl, this.onTap, }) : super(key: key); override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ ClipRRect( borderRadius: BorderRadius.vertical(top: Radius.circular(12)), child: Image.network( imageUrl, height: 200, fit: BoxFit.cover, ), ), Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), SizedBox(height: 8), Text( subtitle, style: TextStyle( fontSize: 14, color: Colors.black54, ), ), ], ), ), ], ), ), ); } } // 使用 class CardWithImageExample extends StatelessWidget { override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(图片卡片示例)), body: Padding( padding: const EdgeInsets.all(16.0), child: GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16, childAspectRatio: 0.8, ), itemCount: 4, itemBuilder: (context, index) { return CardWithImage( title: 卡片 $index, subtitle: 这是卡片 $index 的描述, imageUrl: https://picsum.photos/200/300?random$index, onTap: () print(点击了卡片 $index), ); }, ), ), ); } }2. 动画组件class AnimatedButton extends StatefulWidget { final String text; final VoidCallback onPressed; const AnimatedButton({ Key? key, required this.text, required this.onPressed, }) : super(key: key); override _AnimatedButtonState createState() _AnimatedButtonState(); } class _AnimatedButtonState extends StateAnimatedButton with SingleTickerProviderStateMixin { late AnimationController _controller; late Animationdouble _scaleAnimation; bool _isPressed false; override void initState() { super.initState(); _controller AnimationController( duration: Duration(milliseconds: 200), vsync: this, ); _scaleAnimation Tweendouble(begin: 1, end: 0.95).animate( CurvedAnimation( parent: _controller, curve: Curves.easeOut, ), ); } void _handleTapDown(TapDownDetails details) { setState(() { _isPressed true; }); _controller.forward(); } void _handleTapUp(TapUpDetails details) { setState(() { _isPressed false; }); _controller.reverse(); widget.onPressed(); } void _handleTapCancel() { setState(() { _isPressed false; }); _controller.reverse(); } override Widget build(BuildContext context) { return GestureDetector( onTapDown: _handleTapDown, onTapUp: _handleTapUp, onTapCancel: _handleTapCancel, child: AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: Container( padding: EdgeInsets.symmetric(horizontal: 32, vertical: 12), decoration: BoxDecoration( gradient: LinearGradient( colors: _isPressed ? [Colors.purple, Colors.deepPurple] : [Colors.blue, Colors.purple], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(8), boxShadow: _isPressed ? [] : [ BoxShadow( color: Colors.black.withOpacity(0.2), spreadRadius: 2, blurRadius: 4, offset: Offset(0, 2), ), ], ), child: Text( widget.text, style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ); }, ), ); } override void dispose() { _controller.dispose(); super.dispose(); } } // 使用 class AnimatedButtonExample extends StatelessWidget { override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(动画按钮示例)), body: Center( child: AnimatedButton( text: 点击我, onPressed: () print(按钮被点击), ), ), ); } }3. 表单组件class CustomTextField extends StatefulWidget { final String labelText; final String hintText; final TextEditingController? controller; final FormFieldValidatorString? validator; final bool obscureText; final TextInputType keyboardType; const CustomTextField({ Key? key, required this.labelText, required this.hintText, this.controller, this.validator, this.obscureText false, this.keyboardType TextInputType.text, }) : super(key: key); override _CustomTextFieldState createState() _CustomTextFieldState(); } class _CustomTextFieldState extends StateCustomTextField { bool _isFocused false; override Widget build(BuildContext context) { return TextFormField( controller: widget.controller, validator: widget.validator, obscureText: widget.obscureText, keyboardType: widget.keyboardType, onTap: () { setState(() { _isFocused true; }); }, onChanged: (value) { setState(() { _isFocused true; }); }, onFieldSubmitted: (value) { setState(() { _isFocused false; }); }, decoration: InputDecoration( labelText: widget.labelText, hintText: widget.hintText, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: Colors.blue, width: 2, ), ), filled: true, fillColor: _isFocused ? Colors.blue.shade50 : Colors.grey.shade50, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), ); } } // 使用 class CustomTextFieldExample extends StatelessWidget { final _formKey GlobalKeyFormState(); final _emailController TextEditingController(); final _passwordController TextEditingController(); override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(自定义输入框示例)), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( children: [ CustomTextField( labelText: 邮箱, hintText: 请输入邮箱地址, controller: _emailController, validator: (value) { if (value null || value.isEmpty) { return 请输入邮箱地址; } if (!RegExp(r^[^\s][^\s]\.[^\s]$).hasMatch(value)) { return 请输入有效的邮箱地址; } return null; }, keyboardType: TextInputType.emailAddress, ), SizedBox(height: 20), CustomTextField( labelText: 密码, hintText: 请输入密码, controller: _passwordController, validator: (value) { if (value null || value.isEmpty) { return 请输入密码; } if (value.length 6) { return 密码长度至少为 6 位; } return null; }, obscureText: true, ), SizedBox(height: 30), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { print(邮箱: ${_emailController.text}); print(密码: ${_passwordController.text}); } }, child: Text(提交), ), ], ), ), ), ); } }四、实战案例1. 自定义导航栏class CustomNavigationBar extends StatefulWidget { final ListNavigationItem items; final int currentIndex; final ValueChangedint onTap; const CustomNavigationBar({ Key? key, required this.items, required this.currentIndex, required this.onTap, }) : super(key: key); override _CustomNavigationBarState createState() _CustomNavigationBarState(); } class NavigationItem { final String title; final IconData icon; NavigationItem(this.title, this.icon); } class _CustomNavigationBarState extends StateCustomNavigationBar { override Widget build(BuildContext context) { return Container( height: 60, decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), spreadRadius: 0, blurRadius: 10, offset: Offset(0, -2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: widget.items.asMap().entries.map((entry) { int index entry.key; NavigationItem item entry.value; bool isSelected index widget.currentIndex; return GestureDetector( onTap: () widget.onTap(index), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( item.icon, color: isSelected ? Colors.blue : Colors.grey, size: isSelected ? 24 : 20, ), SizedBox(height: 4), Text( item.title, style: TextStyle( color: isSelected ? Colors.blue : Colors.grey, fontSize: isSelected ? 12 : 10, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), ], ), ); }).toList(), ), ); } } // 使用 class CustomNavigationBarExample extends StatefulWidget { override _CustomNavigationBarExampleState createState() _CustomNavigationBarExampleState(); } class _CustomNavigationBarExampleState extends StateCustomNavigationBarExample { int _currentIndex 0; final ListNavigationItem _items [ NavigationItem(首页, Icons.home), NavigationItem(发现, Icons.explore), NavigationItem(消息, Icons.message), NavigationItem(我的, Icons.person), ]; final ListWidget _pages [ Center(child: Text(首页)), Center(child: Text(发现)), Center(child: Text(消息)), Center(child: Text(我的)), ]; override Widget build(BuildContext context) { return Scaffold( body: _pages[_currentIndex], bottomNavigationBar: CustomNavigationBar( items: _items, currentIndex: _currentIndex, onTap: (index) { setState(() { _currentIndex index; }); }, ), ); } }2. 自定义下拉菜单class CustomDropdown extends StatefulWidget { final ListString items; final String hintText; final ValueChangedString? onChanged; const CustomDropdown({ Key? key, required this.items, required this.hintText, this.onChanged, }) : super(key: key); override _CustomDropdownState createState() _CustomDropdownState(); } class _CustomDropdownState extends StateCustomDropdown { String? _selectedValue; override Widget build(BuildContext context) { return Container( padding: EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), color: Colors.white, ), child: DropdownButtonHideUnderline( child: DropdownButtonString( value: _selectedValue, hint: Text(widget.hintText), onChanged: (value) { setState(() { _selectedValue value; widget.onChanged?.call(value!); }); }, items: widget.items.map((item) { return DropdownMenuItemString( value: item, child: Text(item), ); }).toList(), isExpanded: true, icon: Icon(Icons.arrow_drop_down), iconSize: 24, style: TextStyle( color: Colors.black87, fontSize: 16, ), ), ), ); } } // 使用 class CustomDropdownExample extends StatelessWidget { override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(自定义下拉菜单示例)), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ CustomDropdown( items: [选项 1, 选项 2, 选项 3, 选项 4], hintText: 请选择一个选项, onChanged: (value) { print(选择了: $value); }, ), ], ), ), ); } }五、最佳实践可配置性通过构造函数参数提供灵活的配置选项封装性将组件的内部逻辑封装只暴露必要的接口可重用性设计通用的组件以便在多个地方使用性能优化使用 const 构造器、避免不必要的重建测试为组件编写单元测试确保其稳定性文档为组件添加注释和文档说明其用途和用法六、常见问题1. 组件重构// 重构前 class ComplexWidget extends StatelessWidget { override Widget build(BuildContext context) { return Container( // 复杂的布局逻辑 ); } } // 重构后 class ComplexWidget extends StatelessWidget { override Widget build(BuildContext context) { return Container( child: Column( children: [ _buildHeader(), _buildContent(), _buildFooter(), ], ), ); } Widget _buildHeader() { return Container(/* 头部逻辑 */); } Widget _buildContent() { return Container(/* 内容逻辑 */); } Widget _buildFooter() { return Container(/* 底部逻辑 */); } }2. 状态管理// 使用 StatefulWidget 管理状态 class StatefulComponent extends StatefulWidget { override _StatefulComponentState createState() _StatefulComponentState(); } class _StatefulComponentState extends StateStatefulComponent { bool _isExpanded false; void _toggleExpanded() { setState(() { _isExpanded !_isExpanded; }); } override Widget build(BuildContext context) { return Container(/* 组件逻辑 */); } } // 或使用状态管理库 class StatelessComponent extends ConsumerWidget { override Widget build(BuildContext context, WidgetRef ref) { final isExpanded ref.watch(expandedProvider); return Container(/* 组件逻辑 */); } }3. 性能优化// 使用 const 构造器 class ConstWidget extends StatelessWidget { const ConstWidget({Key? key}) : super(key: key); override Widget build(BuildContext context) { return Container(/* 组件逻辑 */); } } // 使用 RepaintBoundary class ExpensiveWidget extends StatelessWidget { override Widget build(BuildContext context) { return RepaintBoundary( child: Container(/* 复杂绘制逻辑 */), ); } }七、总结Flutter 自定义组件是构建高质量应用的重要工具它允许我们创建符合特定需求的 UI 元素提高代码的可重用性和可维护性。通过掌握自定义组件的高级技巧我们可以创建出更加灵活、美观的 UI 组件。作为一名 UI 匠人我建议在项目中合理使用自定义组件让代码更加清晰、可维护。自定义组件是 Flutter 开发的核心它让我们能够创建出更加符合特定需求的 UI 元素。#flutter #custom-widgets #ui #dart #frontend

更多文章