import 'package:flutter/material.dart'; /// 懒加载 + 保活的 IndexedStack(底部 tab 内容页标准写法) /// /// 没进过的 tab 用空盒占位,首次切到才真正 build;进过一次就一直留在树里, /// 切走只是不 paint,state / 滚动位置 / 播放器全部保留。 /// 相比 PageView:不需要 AutomaticKeepAlive 保活,也不会预建相邻页。 /// /// 非当前 tab 用 TickerMode(enabled: false) 关掉:IndexedStack 只是不 paint, /// 离屏页的动画/定时器照样在跑(跑马灯、Shimmer、轮播),看不见还空转。 class LazyIndexedStack extends StatefulWidget { final int index; final List children; const LazyIndexedStack({super.key, required this.index, required this.children}); @override State createState() => _LazyIndexedStackState(); } class _LazyIndexedStackState extends State { //已建过的下标:进过一次就永久保活,不再退回占位 final _loaded = {}; @override void initState() { super.initState(); _loaded.add(widget.index); } @override void didUpdateWidget(covariant LazyIndexedStack oldWidget) { super.didUpdateWidget(oldWidget); _loaded.add(widget.index); } @override Widget build(BuildContext context) { return IndexedStack( // 必须 expand:默认的 loose 会让子页拿到非 tight 约束,从而不再是 relayout boundary, // 离屏页一脏就把整个 stack 拖着重新布局(PageView 原来给的是 tight,每页各自隔离)。 // 顺带保证「根节点自身不撑满」的页也能满屏,不依赖各页自觉 sizing: StackFit.expand, index: widget.index, children: List.generate( widget.children.length, (i) => _loaded.contains(i) ? TickerMode(enabled: i == widget.index, child: widget.children[i]) : const SizedBox.shrink(), ), ); } }