鸿蒙Flutter实战:IndexedStack保持Tab页面状态

发布时间:2026/6/4 9:56:14

鸿蒙Flutter实战:IndexedStack保持Tab页面状态 前言底部导航栏切换 Tab 是移动应用最常见的交互模式之一。但 Flutter 初学者容易踩一个坑切换 Tab 后切回来之前页面的滚动位置、输入内容、选中状态全丢了——页面被重建了。这是因为 Flutter 的 widget 是声明式的当BottomNavigationBar切换索引时如果使用_pages[_currentIndex]或if/switch切换子 widget旧的 widget 会被 dispose新的被创建。解决方案是IndexedStack——它同时持有所有子 widget但只渲染当前索引的那一个。本文拆解IndexedStack的工作原理、性能影响和使用场景。项目仓库todo_flutter_harmony问题重现错误写法// 错误写法——每次切换都会重建页面classHomePageextendsStatefulWidget{overrideStateHomePagecreateState()_HomePageState();}class_HomePageStateextendsStateHomePage{int _currentIndex0;overrideWidgetbuild(BuildContextcontext){returnScaffold(body:_buildPage(_currentIndex),// 直接调用方法bottomNavigationBar:BottomNavigationBar(currentIndex:_currentIndex,onTap:(index){setState(()_currentIndexindex);},items:const[...],),);}Widget_buildPage(int index){switch(index){case0:returnconstMemoListPage();case1:returnconstTodoListPage();case2:returnconstDiaryListPage();case3:returnconstStatsPage();default:returnconstSizedBox();}}}问题当_currentIndex从 0 变为 1 时_buildPage(0)返回的MemoListPagewidget 从树中被移除其State被dispose。当切回 0 时一个新的MemoListPage被创建所有状态丢失。IndexedStack正确的写法classHomePageextendsStatefulWidget{overrideStateHomePagecreateState()_HomePageState();}class_HomePageStateextendsStateHomePage{int _currentIndex0;overrideWidgetbuild(BuildContextcontext){returnScaffold(body:IndexedStack(index:_currentIndex,children:const[MemoListPage(),TodoListPage(),DiaryListPage(),StatsPage(),],),bottomNavigationBar:NavigationBar(selectedIndex:_currentIndex,onDestinationSelected:(index){setState(()_currentIndexindex);},destinations:const[NavigationDestination(icon:Icon(Icons.note_alt_outlined),label:备忘录),NavigationDestination(icon:Icon(Icons.checklist_outlined),label:待办),NavigationDestination(icon:Icon(Icons.book_outlined),label:日记),NavigationDestination(icon:Icon(Icons.bar_chart),label:统计),],),);}}IndexedStack的工作原理所有children都被创建并保持在 widget 树中只有children[index]被渲染可见未渲染的 children 仍然存活其State不会被 dispose切换到另一个 index 时之前被隐藏的 child 变成可见但其 State 原封不动IndexedStack vs 其他方案方案状态保持内存占用首次加载IndexedStack✅ 全部保持高所有页面常驻所有页面同时初始化PageViewAutomaticKeepAliveClientMixin✅ 按需保持中懒加载Offstage✅ 保持但仍在树中高所有页面同时初始化Visibility❌ 不保持低每次重建if/switch❌ 不保持低每次重建AutomaticKeepAliveClientMixin方案classMemoListPageextendsStatefulWidget{overrideStateMemoListPagecreateState()_MemoListPageState();}class_MemoListPageStateextendsStateMemoListPagewithAutomaticKeepAliveClientMixin{overrideboolgetwantKeepAlivetrue;// 关键overrideWidgetbuild(BuildContextcontext){super.build(context);// 必须调用return...;}}配合PageView使用PageView(controller:_pageController,children:const[MemoListPage(),TodoListPage(),DiaryListPage(),StatsPage(),],)这个方案的优点是页面可以懒加载切到该页才初始化但代码更复杂。对于只有 4 个 Tab 的备忘录应用IndexedStack的简洁性胜出。内存分析4 个页面同时存活会占多少内存MemoListPage一个 ListView 若干 Provider Consumer约 2-3MBTodoListPage同上约 2-3MBDiaryListPage同约 2-3MBStatsPage一个 Grid 布局 热力图约 3-5MB总计约 10-15MB。对于现代手机通常 4GB RAM这个内存开销完全可以接受。数据加载时机使用IndexedStack时所有 4 个页面在首次创建时都会执行initState。这意味着 4 个 Provider 的数据加载会同时触发// MemoListPage.initStateWidgetsBinding.instance.addPostFrameCallback((_){context.readMemoProvider().loadMemos();});// TodoListPage.initStateWidgetsBinding.instance.addPostFrameCallback((_){context.readTodoProvider().loadTodos();});// DiaryListPage.initStateWidgetsBinding.instance.addPostFrameCallback((_){context.readDiaryProvider().loadDiaries();});// StatsPage.initState —— 不需要额外加载数据来自其他 3 个 Provider4 个addPostFrameCallback都在同一帧中注册在下一帧一起触发。由于每个 Provider 独立加载自己的数据读 JSON 文件、解析、notifyListeners它们之间是串行但在 event loop 上快速连续执行。对于几百 KB 的 JSON 文件总加载时间 50ms。统计页的特殊处理统计页的数据来自另外 3 个 Provider——它不需要自己加载数据而是 watch 另外 3 个classStatsPageextendsStatelessWidget{overrideWidgetbuild(BuildContextcontext){finalmemoProvidercontext.watchMemoProvider();finaltodoProvidercontext.watchTodoProvider();finaldiaryProvidercontext.watchDiaryProvider();returnSingleChildScrollView(padding:constEdgeInsets.all(16),child:Column(children:[_buildStatsGrid(memoProvider,todoProvider,diaryProvider),constSizedBox(height:20),_buildCompletionProgress(todoProvider),constSizedBox(height:20),_buildMoodDistribution(diaryProvider),constSizedBox(height:20),_buildDiaryHeatmap(diaryProvider),],),);}}当用户在备忘录 Tab 新增了一条备忘录切换到统计 Tab 时统计页的context.watchMemoProvider()已经持有了最新数据——因为MemoProvider的状态在切换前就更新了。鸿蒙兼容性IndexedStack是 Flutter 框架层的基础组件完全在 Dart/渲染引擎层实现。不涉及任何平台 API与鸿蒙 OHOS 零冲突。总结IndexedStack是 Tab 切换保持页面状态的最简方案所有子页面同时创建并存活切换时不销毁不重建只有当前 index 的子页面被渲染其他页面保持存活但不可见内存开销 ~10-15MB对现代设备可接受AutomaticKeepAliveClientMixin是更灵活但更复杂的替代方案4 行代码IndexedStack 4 个children解决了一个常见且恼人的用户体验问题。完整项目代码见todo_flutter_harmony

相关新闻