项目背景与需求
在Flutter开发中,我们经常遇到需要实现多Tab栏列表的场景,每个Tab具有相似的结构(下拉刷新、上拉加载、列表展示),但数据类型和UI展示各不相同。本文将介绍如何使用模板方法模式来优雅地解决这个问题。
模板方法模式简介
模板方法模式是一种行为设计模式,它在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写特定步骤。在我们的场景中,每个Tab的加载流程(初始化→刷新→加载更多)是相同的,但具体的数据加载和UI展示是不同的。
代码实现解析
1. 抽象基类定义
const int defaultPageSize = 10;
abstract class TabData<T> {
final String name; // tab名称
int total; // 数据数量
bool isSelected; // 选中状态
bool hasMore; // 是否有更多数据
List<T> items; // 数据列表
bool _isInited = false; // 是否初始化完成
final RefreshController controller;
final Widget Function(BuildContext context,T item) itemBuilder;
TabData({
required this.name,
required this.itemBuilder,
this.total = 0,
this.isSelected = false,
this.hasMore = true,
this.items = const []
}) : controller = RefreshController();
int get pageSize => defaultPageSize;
Future<List<T>> loadData(int page);
void updateItems(List<T> newItems, bool isRefresh);
Future<bool> init() async{
if (_isInited) {
return true;
}
_isInited = true;
await onRefresh();
return false;
}
Future<void> onRefresh() async {
try {
final data = await loadData(1);
updateItems(data, true);
hasMore = data.length >= pageSize;
controller.refreshCompleted();
} catch (e) {
controller.refreshFailed();
rethrow;
}
}
Future<void> onLoadMore() async {
if (!hasMore) {
controller.loadNoData();
return;
}
try {
final currentPage = (items.length / pageSize).ceil() + 1;
final data = await loadData(currentPage);
updateItems(data, false);
hasMore = data.length >= pageSize;
controller.loadComplete();
} catch (e) {
controller.refreshFailed();
rethrow;
}
}
void dispose() {
controller.dispose();
}
}设计要点:
- 定义了完整的生命周期方法(init、onRefresh、onLoadMore)
- 使用泛型T支持不同的数据类型
- 封装了刷新控制器和分页逻辑
- 提供itemBuilder回调实现UI定制化
2. 具体子类实现
点赞Tab实现:
class LikeTabData extends TabData<LikeTabDataEntity>{
LikeTabData():super(name: "点赞",itemBuilder: (context,item){
return Text(item.name);
});
@override
Future<List<LikeTabDataEntity>> loadData(int page) async{
await Future.delayed(const Duration(seconds: 1));
return List.generate(pageSize, (index)=> LikeTabDataEntity(name: "$name 数据 ${page * pageSize + index}"));
}
@override
void updateItems(List<LikeTabDataEntity> newItems, bool isRefresh) {
items = isRefresh ? newItems : [...items, ...newItems];
total = items.length;
}
}评论Tab实现:
class CommentTabData extends TabData<CommentTabDataEntity>{
CommentTabData():super(name: "评论",itemBuilder: (context,item) {
Widget text () => Text(item.text,style: const TextStyle(color: Colors.white));
return Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(8),
),
child: text(),
);
});
@override
Future<List<CommentTabDataEntity>> loadData(int page) async{
await Future.delayed(const Duration(seconds: 1));
return List.generate(pageSize, (index)=> CommentTabDataEntity(text: "$name 数据 ${page * pageSize + index}"));
}
@override
void updateItems(List<CommentTabDataEntity> newItems, bool isRefresh) {
items = isRefresh ? newItems : [...items, ...newItems];
total = items.length;
}
}子类只需实现:
- loadData:具体的数据加载逻辑
- updateItems:数据合并策略
- itemBuilder:列表项UI构建
3. 状态管理
class DialogState extends BaseViewState {
List<TabData> tabDataList = [
LikeTabData(),
CommentTabData(),
LikeTabData(),
CommentTabData(),
];
}4. 逻辑控制器
class DialogLogic extends BaseGetXLifeCycleController{
late final PageController tabController;
final state = DialogState();
@override
void onInit() {
super.onInit();
tabController = PageController();
state.viewState = TYPE_LOAD;
update();
state.tabDataList.first.init().then((_)=> update());
Future.delayed(const Duration(seconds: 1),(){
state.tabDataList.first.isSelected = true;
state.viewState = TYPE_NORMAL;
update();
});
}
@override
void onClose() {
tabController.dispose();
for(final item in state.tabDataList) {
item.dispose();
}
super.onClose();
}
void onTabChange(int index) async{
for(int i = 0; i < state.tabDataList.length; i++) {
state.tabDataList[i].isSelected = index == i;
}
update();
final isInited = await state.tabDataList[index].init();
if(!isInited) update();
}
void changeTab(int index,BuildContext context){
final screenWidth = MediaQuery.of(context).size.width;
tabController.animateTo(index * screenWidth,duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
}
void onRefresh(Future<void> Function() refresh) async{
await refresh();
update();
}
void onLoadMore(Future<void> Function() loadMore) async{
await loadMore();
update();
}
}5. UI界面实现
class Dialog extends BaseTitleBottomSheet with GetXControllerMixin<DialogLogic>{
Dialog({super.key});
final logic = Get.put(DialogLogic());
final state = Get.find<DialogLogic>().state;
@override
String getTitle() => "Dialog";
@override
Widget buildContent() {
return buildPartBaseView(
state.viewState,context,
_buildContentWidget()
);
}
Widget _buildContentWidget() {
return Expanded(
child: Column(
children: [
SingleChildScrollView(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for(int i = 0;i<state.tabDataList.length;i++)
GestureDetector(onTap: ()=> logic.changeTab(i,context),child: _buildTabWidget(state.tabDataList[i]))
],
),
),
Expanded(
child: PageView.builder(
controller: logic.tabController,
onPageChanged: logic.onTabChange,
itemCount: state.tabDataList.length,
itemBuilder: (_,index){
final data = state.tabDataList[index];
if(data.items.isEmpty){
return const Center(child: Text("无数据"),);
}
return _KeepAliveList(
data,
keepAlive: true,
onLoading: ()=> logic.onLoadMore(data.onLoadMore),
onRefresh: ()=> logic.onRefresh(data.onRefresh),
controller: data.controller
);
},
),
),
],
),
);
}
Widget _buildTabWidget(TabData entity){
final noSelectedStyle = TextStyle(color: Colors.black,fontSize: 23.w);
final selectedStyle = TextStyle(color: Colors.blueAccent,fontSize: 23.w);
final style = entity.isSelected ? selectedStyle : noSelectedStyle;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(entity.name,style: style),
Text("(${entity.total})",style: style),
],
);
}
}
class _KeepAliveList extends StatefulWidget {
final TabData tabData;
final bool keepAlive;
final VoidCallback? onRefresh;
final VoidCallback? onLoading;
final RefreshController controller;
const _KeepAliveList(this.tabData,{super.key,this.keepAlive = true,required this.controller,this.onRefresh,this.onLoading});
@override
State<_KeepAliveList> createState() => _KeepAliveListState();
}
class _KeepAliveListState extends State<_KeepAliveList> with AutomaticKeepAliveClientMixin{
@override
Widget build(BuildContext context) {
super.build(context);
return SmartRefresher(
controller: widget.controller,
enablePullDown: true,
enablePullUp: true,
header: const ClassicHeader(),
onRefresh: widget.onRefresh,
onLoading: widget.onLoading,
child: ListView.builder(
itemCount: widget.tabData.items.length,
itemBuilder: (context,index){
// 新版本dart可以使用switch匹配模式,可以穷举完全部密封类的子类
// 为了兼容只能使用if了
final tabData = widget.tabData;
if(tabData is LikeTabData){
return tabData.itemBuilder(context,tabData.items[index]);
}else if(tabData is CommentTabData){
return tabData.itemBuilder(context,tabData.items[index]);
}
throw "未知的TabData类型";
},
),
);
}
@override
bool get wantKeepAlive => widget.keepAlive;
}设计优势
1. 代码复用性
- 公共的刷新、加载、分页逻辑在基类中实现
- 新增Tab只需继承TabData并实现三个抽象方法
2. 可维护性
- 算法框架稳定,修改基类即可影响所有子类
- 业务逻辑与UI展示分离
3. 扩展性
- 支持任意类型的列表数据
- 可轻松添加新的Tab类型
4. 一致性
- 所有Tab保持相同的交互逻辑
- 统一的错误处理和加载状态
使用建议
- 添加新Tab:只需创建新的TabData子类,实现loadData、updateItems和itemBuilder
- 修改公共逻辑:在TabData基类中修改,所有子类自动生效
- 自定义分页大小:在子类中重写pageSize getter
- 特殊处理:子类可重写onRefresh或onLoadMore方法进行个性化处理
总结
通过模板方法模式,我们成功地将多Tab列表中的公共逻辑提取到基类中,同时保留了子类的灵活性。这种设计模式特别适用于具有相似流程但不同实现的场景,能够显著减少代码重复,提高项目的可维护性和扩展性。
评论 (0)