项目背景:
项目使用了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才会包含1,if判断才会通过,进而触发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的一个核心方法。
检查缓存 :它会先去
_pageCache中查找键为index的项。如果存在(缓存命中) :它会直接返回
_pageCache中已有的那个Widget实例。如果不存在(缓存未命中) :它会执行 你传入的
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]
)
}