Flutter模板方法模式在多Tab列表中的应用

Flutter模板方法模式在多Tab列表中的应用

尽意
2026-01-16 / 0 评论 / 3 阅读 / 正在检测是否收录...

项目背景与需求

在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保持相同的交互逻辑
  • 统一的错误处理和加载状态

使用建议

  1. 添加新Tab:只需创建新的TabData子类,实现loadData、updateItems和itemBuilder
  2. 修改公共逻辑:在TabData基类中修改,所有子类自动生效
  3. 自定义分页大小:在子类中重写pageSize getter
  4. 特殊处理:子类可重写onRefresh或onLoadMore方法进行个性化处理

总结

通过模板方法模式,我们成功地将多Tab列表中的公共逻辑提取到基类中,同时保留了子类的灵活性。这种设计模式特别适用于具有相似流程但不同实现的场景,能够显著减少代码重复,提高项目的可维护性和扩展性。

1

评论 (0)

取消