Flutter 高性能 Tab 导航:懒加载与状态保持的最佳实践

项目背景:

项目使用了Getx框架进行状态管理,该文章是为了搭建一个高性能的tab切换的首页框架。

main_tab_view.dart 核心代码:

复制代码
class MainTabPage extends StatelessWidget {
  MainTabPage({Key? key}) : super(key: key);

  final MainTabLogic logic = Get.put(MainTabLogic());


  final List<Widget Function()> _pageBuilders = [
    () => HomePage(),
    () => OrderPage(),
    () => AiQusPage(),
    () => SettingPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Obx(() => Scaffold(
      body: PageView.builder(
        controller: logic.pageController,
        onPageChanged: logic.onPageChanged,
        itemCount: logic.pageCount,
        itemBuilder: (context, index) {
          // 懒加载:只有访问过的页面才会被创建
          if (logic.isPageVisited(index)) {
            return _KeepAlivePage(
              child: logic.getOrCreatePage(index, _pageBuilders[index]),
            );
          } else {
            // 未访问的页面返回空白占位
            return const SizedBox.shrink();
          }
        },
      ),
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: logic.currentIndex.value,
        onTap: logic.changeTab,
        type: BottomNavigationBarType.fixed,
        backgroundColor: Colors.white,
        selectedItemColor: const Color(0xFF4A90E2),
        unselectedItemColor: const Color(0xFF999999),
        selectedFontSize: 12.sp,
        unselectedFontSize: 12.sp,
        elevation: 8,
        items: [
          BottomNavigationBarItem(
            icon: Icon(Icons.home_outlined, size: 24.w),
            activeIcon: Icon(Icons.home, size: 24.w),
            label: '首页',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.article_outlined, size: 24.w),
            activeIcon: Icon(Icons.article, size: 24.w),
            label: '订阅',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.construction_outlined, size: 24.w),
            activeIcon: Icon(Icons.construction, size: 24.w),
            label: '工具',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings_outlined, size: 24.w),
            activeIcon: Icon(Icons.settings, size: 24.w),
            label: '设置',
          ),
        ],
      ),
    ));
  }
}

/// 保持页面状态的包装器
/// 使用 AutomaticKeepAliveClientMixin 确保页面切换时不被销毁
class _KeepAlivePage extends StatefulWidget {
  final Widget child;

  const _KeepAlivePage({required this.child});

  @override
  State<_KeepAlivePage> createState() => _KeepAlivePageState();
}

class _KeepAlivePageState extends State<_KeepAlivePage>
    with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context); // 必须调用,保持状态的关键
    return widget.child;
  }
}

class MainTabLogic extends GetxController {
  // 当前选中的Tab索引
  final RxInt currentIndex = 0.obs;

  // PageView 控制器
  late PageController pageController;

  // 页面缓存 - 只缓存访问过的页面
  final Map<int, Widget> _pageCache = {};

  // 记录哪些页面已经被访问过
  final RxSet<int> visitedPages = <int>{0}.obs; // 首页默认访问

  // 页面总数
  final int pageCount = 4;

  @override
  void onInit() {
    super.onInit();
    // 初始化 PageController
    pageController = PageController(initialPage: 0);
  }

  /// 切换Tab(带动画)
  void changeTab(int index) {
    if (index >= 0 && index < pageCount && index != currentIndex.value) {
      currentIndex.value = index;
      visitedPages.add(index); // 标记为已访问
      // 跳转到指定页面,带动画
      pageController.animateToPage(
        index,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeInOut,
      );
    }
  }

  /// PageView 滑动时的回调
  void onPageChanged(int index) {
    currentIndex.value = index;
    visitedPages.add(index); // 标记为已访问
  }

  /// 获取或创建页面(懒加载)
  Widget getOrCreatePage(int index, Widget Function() builder) {
    return _pageCache.putIfAbsent(index, builder);
  }

  /// 检查页面是否已访问
  bool isPageVisited(int index) {
    return visitedPages.contains(index);
  }

  @override
  void onClose() {
    pageController.dispose();
    _pageCache.clear();
    visitedPages.clear();
    super.onClose();
  }
}

关键点:

1.延迟实例化:使用 Widget Function()
复制代码
final List<Widget Function()> _pageBuilders = [
  () => HomePage(),
  () => OrderPage(),
  () => AiQusPage(),
  () => SettingPage(),
];

这个列表是一个返回widget的函数列表,而不是一个widget实例的列表

如果列表定义为:

复制代码
final List<Widget> _pages = [
  HomePage(), // 立即创建
  OrderPage(), // 立即创建
  // ...
];

MainTabPage被创建时它会立即初始化它的所有成员变量,为了完成这个初始化,Dart必须执行等号右边的代码 [HomePage(), OrderPage(), ...]

执行 HomePage() 这个表达式(即调用它的构造函数)的过程,就叫做**"实例化",它会在内存中创建一个HomePage 实例**(Instance)或对象(Object)。

而方案一,成员变量中是一个匿名函数,他并不是页面本身,而是一个会返回页面widget的函数。只有在调用这个函数的时候,函数返回的时候才会去创建实例。

2. 条件懒加载:利用 visitedPages + PageView.builder 实现懒加载
复制代码
final RxSet<int> visitedPages = <int>{0}.obs; // 首页默认访问

itemBuilder: (context, index) {
  // 懒加载:只有访问过的页面才会被创建
  if (logic.isPageVisited(index)) {
    return _KeepAlivePage(
      child: logic.getOrCreatePage(index, _pageBuilders[index]),
    );
  } else {
    // 未访问的页面返回空白占位
    return const SizedBox.shrink();
  }
},

PageView 默认会预加载相邻的页面,但在这里不希望这么做。

isitedPages (一个响应式的Set) 负责精确追踪用户到底访问了哪些页面。

  • 当用户点击Tab(changeTab)或滑动页面(onPageChanged)时,才会将对应的index添加到visitedPages中。

  • itemBuilder 中的 if (logic.isPageVisited(index)) 判断是关键:

    • PageView 可能会尝试构建(build)索引为 1 的页面(即使用从没点过第2个tab),但因为 visitedPages 中不包含 1,这个 if 判断会失败。

    • return const SizedBox.shrink();:对于从未被用户主动访问过的页面,我们返回一个零大小的空盒子。这几乎不消耗任何渲染资源

    • 只有当用户主动访问 过(例如点击了Tab 1),visitedPages 才会包含 1if 判断才会通过,进而触发 getOrCreatePage 去(从缓存或新建)获取页面

3. 缓存与保活:使用 Map 缓存与 KeepAlive 保持状态

保活:

  • PageView 在页面滑出视口(off-screen)时,默认会销毁(dispose)页面的State对象以节省内存。

  • AutomaticKeepAliveClientMixin 是 Flutter 提供的官方解决方案。

  • wantKeepAlive 返回 true 时,它会向 PageView(内部是SliverList)发送一个"保活"信号。

缓存:

复制代码
final Map<int, Widget> _pageCache = {};

Widget getOrCreatePage(int index, Widget Function() builder) {
  return _pageCache.putIfAbsent(index, builder);
}

_pageCache:这是一个Map(字典),用于存储已经创建过的页面Widget实例。它的键(Key)是页面的索引(如0, 1, 2, 3),值(Value)是对应的Widget(如HomePage(), OrderPage())。

putIfAbsent(index, builder):这是Dart中Map的一个核心方法。

  1. 检查缓存 :它会先去 _pageCache 中查找键为 index 的项。

  2. 如果存在(缓存命中) :它会直接返回 _pageCache 中已有的那个Widget实例。

  3. 如果不存在(缓存未命中) :它会执行 你传入的 builder 函数(这个函数就是 () => HomePage() 这样的匿名函数),这个函数会创建新的Widget实例(例如 new HomePage())。然后,putIfAbsent 会将这个新创建的Widget实例存入 _pageCache[index],最后返回这个新创建的实例。

这是一个高级用法

  • 保证实例唯一性 :它确保了每个Tab对应的页面Widget在整个App生命周期中只被创建一次

  • 配合_KeepAlivePage实现状态保持 :当你切换回一个已经访问过的Tab时,你得到的_pageCache中的Widget完全相同的实例 。Flutter的Element树和State树在比对(diff)时,会发现这是同一个Widget,因此会重用它之前的State,从而保留了页面状态(如列表的滚动位置、表单输入等)。

tab切换的完整调用链

复制代码
/ 1. 用户点击"订阅" Tab
BottomNavigationBar.onTap(1)
  ↓
// 2. 调用 changeTab
logic.changeTab(1)
  visitedPages.add(1)  // visitedPages = {0, 1}
  pageController.animateToPage(1)
  ↓
// 3. PageView 动画触发 itemBuilder
PageView.builder.itemBuilder(context, 1)
  ↓
// 4. 检查是否已访问
if (logic.isPageVisited(1)) {  // true
  ↓
  // 5. 返回 KeepAlivePage
  return _KeepAlivePage(
    child: logic.getOrCreatePage(1, _pageBuilders[1])
           ↓
           // 6. 调用 getOrCreatePage
           _pageCache.putIfAbsent(1, () => OrderPage())
                                     ↓
                                     // 7a. 如果缓存不存在
                                     执行: () => OrderPage()
                                     创建: new OrderPage()
                                     存入: _pageCache[1] = OrderPage实例
                                     返回: OrderPage实例
                                     
                                     // 7b. 如果缓存已存在
                                     直接返回: _pageCache[1]
  )
}
相关推荐
wudl55663 小时前
JDK 21 API增强详解
java·开发语言·windows
β添砖java3 小时前
JS基础Day01
开发语言·javascript·ecmascript
cg_ssh3 小时前
Docker 中使用Nginx 一个端口启动多个前端项目
1024程序员节
学习编程的Kitty3 小时前
JavaEE初阶——多线程(3)线程安全
java·开发语言·jvm
Skrrapper3 小时前
【C++】C++ 中的 map
开发语言·c++
古一|4 小时前
Java 前后端加密与编码技术:从概念到实战场景全解析
1024程序员节
艺杯羹4 小时前
解决 Word四大烦:消标记、去波浪线、关首字母大写、禁中文引号
word·文档·1024程序员节·word技巧
寄思~4 小时前
python批量读取word表格写入excel固定位置
开发语言·python·excel
workflower5 小时前
微软PM的来历
java·开发语言·算法·microsoft·django·结对编程