导语
Flutter 组件化开发的核心是 "复用与统一"------ 重复编写按钮、列表等基础组件会导致代码冗余、样式不统一、维护成本高。本文聚焦两个高频通用组件(通用按钮CommonButton、下拉刷新列表RefreshList)的封装,从核心需求拆解、代码实现到实战应用,完整讲解 Widget 封装的核心技巧(参数设计、样式统一、状态隔离、回调规范),让你的组件开箱即用、灵活扩展!
一、封装通用按钮:CommonButton(一站式按钮解决方案)
核心需求拆解
一个高复用的按钮需覆盖业务中绝大多数场景:✅ 支持多类型(主要 / 次要 / 文本按钮)✅ 兼容加载中、禁用状态✅ 自定义尺寸、圆角、颜色✅ 统一点击回调逻辑✅ 样式自适应状态变化(禁用 / 加载)
1. 完整代码实现(带详细注释)
dart
import 'package:flutter/material.dart';
/// 按钮类型枚举(语义化区分按钮样式)
enum ButtonType {
primary, // 主要按钮(强调操作,如提交、确认)
secondary, // 次要按钮(辅助操作,如取消、返回)
text, // 文本按钮(轻量化操作,如查看更多)
}
/// 通用按钮组件(支持多类型、加载/禁用状态、自定义样式)
class CommonButton extends StatelessWidget {
// 核心必选参数
final String text; // 按钮文字
final VoidCallback? onPressed; // 点击回调(null时自动禁用)
// 可选参数(带默认值,降低使用成本)
final ButtonType type; // 按钮类型
final bool isLoading; // 是否加载中(加载时禁用点击)
final bool isDisabled; // 是否禁用
final double? width; // 宽度(默认撑满父容器)
final double height; // 高度(默认48px,符合移动端交互规范)
final double borderRadius; // 圆角(默认8px)
final Color? backgroundColor; // 自定义背景色(覆盖默认类型色)
final Color? textColor; // 自定义文字色(覆盖默认类型色)
final double fontSize; // 字体大小(默认16px)
const CommonButton({
super.key,
required this.text,
this.onPressed,
this.type = ButtonType.primary,
this.isLoading = false,
this.isDisabled = false,
this.width,
this.height = 48,
this.borderRadius = 8,
this.backgroundColor,
this.textColor,
this.fontSize = 16,
});
/// 封装按钮样式逻辑(隔离样式计算,便于维护)
ButtonStyle _getButtonStyle() {
// 基础颜色配置(按类型赋值)
Color bgColor = Colors.transparent;
Color txtColor = Colors.black87;
switch (type) {
case ButtonType.primary:
bgColor = backgroundColor ?? Colors.blue;
txtColor = textColor ?? Colors.white;
break;
case ButtonType.secondary:
bgColor = backgroundColor ?? Colors.grey.shade200;
txtColor = textColor ?? Colors.black87;
break;
case ButtonType.text:
bgColor = Colors.transparent;
txtColor = textColor ?? Colors.blue;
break;
}
// 禁用/加载状态颜色适配(视觉弱化)
if (isDisabled || isLoading) {
bgColor = type == ButtonType.primary
? Colors.blue.shade300
: (type == ButtonType.text ? Colors.transparent : Colors.grey.shade100);
txtColor = type == ButtonType.text
? Colors.grey.shade400
: Colors.grey.shade500;
}
// 统一返回按钮样式
return ElevatedButton.styleFrom(
backgroundColor: bgColor,
foregroundColor: txtColor,
minimumSize: Size(width ?? double.infinity, height), // 宽度默认撑满
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(borderRadius),
),
disabledBackgroundColor: bgColor, // 禁用状态背景色(避免默认灰色)
disabledForegroundColor: txtColor, // 禁用状态文字色
elevation: type == ButtonType.text ? 0 : 2, // 文本按钮无阴影
);
}
@override
Widget build(BuildContext context) {
// 加载中状态:显示加载动画 + 文字
Widget buttonChild = isLoading
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
),
const SizedBox(width: 10),
Text(text, style: TextStyle(fontSize: fontSize)),
],
)
: Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: type == ButtonType.primary ? FontWeight.w500 : FontWeight.normal,
),
);
return ElevatedButton(
onPressed: (isDisabled || isLoading) ? null : onPressed, // 禁用/加载时不可点击
style: _getButtonStyle(),
child: buttonChild,
);
}
}
2. 实战使用示例(覆盖全场景)
dart
class CommonButtonDemo extends StatelessWidget {
const CommonButtonDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('通用按钮示例')),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
children: [
// 1. 主要按钮(默认类型)
CommonButton(
text: '主要按钮',
onPressed: () => debugPrint('主要按钮点击'),
),
const SizedBox(height: 10),
// 2. 次要按钮
CommonButton(
text: '次要按钮',
onPressed: () => debugPrint('次要按钮点击'),
type: ButtonType.secondary,
),
const SizedBox(height: 10),
// 3. 文本按钮
CommonButton(
text: '文本按钮',
onPressed: () => debugPrint('文本按钮点击'),
type: ButtonType.text,
),
const SizedBox(height: 10),
// 4. 加载中按钮
CommonButton(
text: '提交中...',
isLoading: true,
type: ButtonType.primary,
),
const SizedBox(height: 10),
// 5. 禁用按钮
CommonButton(
text: '禁用按钮',
isDisabled: true,
type: ButtonType.primary,
),
const SizedBox(height: 10),
// 6. 自定义样式按钮(覆盖默认颜色/尺寸)
CommonButton(
text: '自定义按钮',
onPressed: () => debugPrint('自定义按钮点击'),
backgroundColor: Colors.green,
textColor: Colors.white,
width: 200,
height: 44,
borderRadius: 22,
fontSize: 18,
),
],
),
),
);
}
}
✅ 按钮封装优化亮点
- 语义化枚举 :通过
ButtonType区分按钮类型,代码可读性更高,避免魔法值。 - 状态自适应:禁用 / 加载状态自动调整颜色,无需外部重复判断。
- 默认值合理:宽度默认撑满、高度 48px、圆角 8px,符合移动端设计规范。
- 扩展性强:预留自定义颜色 / 尺寸参数,可覆盖默认样式,适配不同业务场景。
- 样式隔离 :将样式计算抽离为
_getButtonStyle方法,避免 build 方法冗余。
二、封装下拉刷新列表:RefreshList(全状态适配)
核心需求拆解
下拉刷新列表是移动端高频组件,需覆盖完整业务场景:✅ 下拉刷新 + 上拉加载更多✅ 加载中 / 空数据 / 加载失败状态✅ 自定义列表项、状态提示文本✅ 防重复加载(加载中不触发二次请求)✅ 滚动监听自动触发加载更多
1. 完整代码实现(带详细注释)
dart
import 'package:flutter/material.dart';
import 'common_button.dart'; // 引入通用按钮
/// 回调函数类型定义(语义化,便于理解)
typedef OnRefresh = Future<void> Function(); // 下拉刷新回调
typedef OnLoadMore = Future<void> Function(); // 上拉加载更多回调
typedef ItemBuilder = Widget Function(dynamic item, int index); // 列表项构建器
/// 下拉刷新列表组件(支持空/加载/错误/加载更多状态)
class RefreshList extends StatefulWidget {
// 必选参数
final List data; // 列表数据源
final ItemBuilder itemBuilder; // 列表项构建方法
final OnRefresh onRefresh; // 下拉刷新回调
// 可选参数(带默认值)
final OnLoadMore? onLoadMore; // 上拉加载更多回调
final bool hasMore; // 是否有更多数据
final String? emptyText; // 空数据提示文本
final String? errorText; // 加载失败提示文本
final bool isLoading; // 整体加载中(首次加载/刷新)
final bool isError; // 加载失败
const RefreshList({
super.key,
required this.data,
required this.itemBuilder,
required this.onRefresh,
this.onLoadMore,
this.hasMore = false,
this.emptyText = '暂无数据',
this.errorText = '加载失败,请重试',
this.isLoading = false,
this.isError = false,
});
@override
State<RefreshList> createState() => _RefreshListState();
}
class _RefreshListState extends State<RefreshList> {
final ScrollController _scrollController = ScrollController(); // 滚动控制器
@override
void initState() {
super.initState();
// 监听滚动位置,触发加载更多
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose(); // 释放控制器,避免内存泄漏
super.dispose();
}
/// 滚动监听:到达底部触发加载更多
void _onScroll() {
// 距离底部100px时触发,避免滚动到底才加载
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100) {
// 防重复加载:有更多数据 + 非加载中 + 有加载更多回调
if (widget.hasMore && !widget.isLoading && widget.onLoadMore != null) {
widget.onLoadMore!();
}
}
}
/// 构建状态页面(空/加载中/加载失败)
Widget _buildStatusWidget() {
if (widget.isLoading) {
// 加载中状态
return const Center(
child: CircularProgressIndicator(),
);
}
if (widget.isError) {
// 加载失败状态(带重试按钮)
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(widget.errorText!),
const SizedBox(height: 20),
CommonButton(
text: '重试',
type: ButtonType.secondary,
width: 120,
height: 40,
onPressed: widget.onRefresh,
),
],
),
);
}
if (widget.data.isEmpty) {
// 空数据状态
return Center(
child: Text(widget.emptyText!),
);
}
// 无状态:空组件
return const SizedBox.shrink();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
color: Colors.blue, // 刷新指示器颜色
onRefresh: widget.onRefresh,
child: ListView.builder(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(), // 确保空数据时也能下拉刷新
itemCount: widget.data.length + (widget.hasMore ? 1 : 0), // 加载更多占位项+1
itemBuilder: (context, index) {
// 1. 加载更多占位项
if (index == widget.data.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
);
}
// 2. 空/加载中/加载失败状态(占满屏幕高度)
if (widget.data.isEmpty || widget.isLoading || widget.isError) {
return SizedBox(
height: MediaQuery.of(context).size.height - 100, // 适配屏幕高度
child: _buildStatusWidget(),
);
}
// 3. 正常列表项
return widget.itemBuilder(widget.data[index], index);
},
),
);
}
}
2. 实战使用示例(模拟真实业务场景)
dart
class RefreshListDemo extends StatefulWidget {
const RefreshListDemo({super.key});
@override
State<RefreshListDemo> createState() => _RefreshListDemoState();
}
class _RefreshListDemoState extends State<RefreshListDemo> {
List<String> _listData = []; // 列表数据
bool _hasMore = true; // 是否有更多数据
bool _isLoading = false; // 加载中
bool _isError = false; // 加载失败
@override
void initState() {
super.initState();
_loadInitialData(); // 初始化加载数据
}
/// 初始化加载数据(模拟网络请求)
Future<void> _loadInitialData() async {
setState(() {
_isLoading = true;
_isError = false;
});
try {
// 模拟1秒网络请求
await Future.delayed(const Duration(seconds: 1));
setState(() {
_listData = List.generate(20, (index) => '列表项 ${index + 1}');
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_isError = true;
});
}
}
/// 下拉刷新(重新加载数据)
Future<void> _onRefresh() async {
await Future.delayed(const Duration(seconds: 1));
setState(() {
_listData = List.generate(20, (index) => '刷新后列表项 ${index + 1}');
_hasMore = true; // 刷新后重置加载更多状态
});
}
/// 上拉加载更多
Future<void> _onLoadMore() async {
if (_isLoading) return; // 防重复加载
setState(() {
_isLoading = true;
});
try {
await Future.delayed(const Duration(seconds: 1));
setState(() {
// 追加10条数据
_listData.addAll(List.generate(
10,
(index) => '加载更多项 ${_listData.length + index + 1}',
));
_isLoading = false;
_hasMore = _listData.length < 50; // 最多加载50条数据
});
} catch (e) {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('下拉刷新列表示例')),
body: RefreshList(
data: _listData,
itemBuilder: (item, index) {
// 自定义列表项
return ListTile(
leading: const Icon(Icons.list_alt, color: Colors.blue),
title: Text(item),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () => debugPrint('点击列表项 $index'),
);
},
onRefresh: _onRefresh,
onLoadMore: _onLoadMore,
hasMore: _hasMore,
isLoading: _isLoading,
isError: _isError,
emptyText: '暂无列表数据~',
errorText: '数据加载失败,点击重试',
),
);
}
}
✅ 列表封装优化亮点
- 防重复加载:加载中不触发二次请求,避免接口重复调用。
- 状态全覆盖:支持加载中 / 空数据 / 加载失败 / 加载更多,适配全业务场景。
- 滚动优化:距离底部 100px 提前触发加载更多,提升用户体验。
- 内存安全 :及时释放
ScrollController,避免内存泄漏。 - 交互友好:空数据时仍可下拉刷新,加载失败提供重试按钮。
三、Widget 封装核心技巧(通用方法论)
| 技巧分类 | 具体实现 | 优势 |
|---|---|---|
| 参数设计 | 区分必选(required)/ 可选参数,提供合理默认值 |
降低使用成本,避免空指针,适配多数场景 |
| 样式统一 | 抽离样式计算逻辑(如_getButtonStyle),枚举管理类型 |
避免重复代码,样式统一可维护 |
| 状态隔离 | 无状态组件(CommonButton)+ 有状态组件(RefreshList)分离 |
状态逻辑内聚,UI 与状态解耦 |
| 回调设计 | 自定义typedef语义化回调类型,统一回调格式 |
代码可读性高,回调逻辑清晰 |
| 扩展性 | 预留自定义属性(如颜色、尺寸、提示文本) | 适配不同业务场景,无需重复封装 |
| 性能优化 | 及时释放控制器、防重复加载、局部刷新 | 避免内存泄漏,提升运行效率 |
| 语义化命名 | 枚举(ButtonType)、回调类型(OnRefresh)、方法名(_onScroll) |
降低协作成本,便于后期维护 |
四、进阶优化建议
- 通用按钮扩展:支持图标按钮、渐变背景、自定义加载动画,适配更多设计风格。
- 刷新列表扩展:添加列表项点击防抖、预加载(提前加载下一页)、滑动删除功能。
- 主题适配 :结合
Theme.of(context)统一按钮 / 列表样式,支持暗黑模式。 - 封装组件库:将通用组件抽离为单独 package,便于多项目复用。
- 单元测试:为通用组件编写测试用例,覆盖不同状态和参数,保证稳定性。
掌握以上封装技巧,可快速打造高复用、易维护的通用组件库,大幅提升 Flutter 开发效率。