Flutter动态TabBarView结合AutomaticKeepAliveClientMixin状态丢失问题
问题描述
我实现了一个动态TabBar,切换至第一个Tab时,会在TabBar头部添加3个新Tab。为保存Tab页面状态,我使用了AutomaticKeepAliveClientMixin,但添加新Tab后,原有页面的状态出现丢失。
现象:原本12/09/2022的Tab显示内容正确,切换到第一个Tab并添加3个新Tab后,12/06/2022的Tab却显示了12/09的内容。
相关代码
TabBloc
class TabBloc extends Bloc<TabEvent, TabState> { TabBloc() : super(TabState.init()) { on<TabEvent>((event, emit) { emit(state.addItem(event.addToEnd, event.currentPosition)); }); } }
TabEvent
class TabEvent { final bool addToEnd; final int currentPosition; TabEvent({required this.addToEnd, required this.currentPosition}); }
TabState
class TabState { late final Tab tab; TabState({required this.tab}); TabState.init() { SplayTreeMap<DateTime, String> items = SplayTreeMap(); for (int i = -2; i <= 2; i++) { var dateTime = DateTime.now().add(Duration(days: i)); items[dateTime] = intl.DateFormat("EEE dd MMM").format(dateTime); } int tabPosition = 2; tab = Tab(count: items.length, items: items, position: tabPosition); } TabState copyWith(newTab) { return TabState(tab: newTab); } TabState addItem(bool addToEnd, int currentPosition) { SplayTreeMap<DateTime, String> items = tab.items; int position = currentPosition; if (addToEnd) { for (int i = 1; i <= 3; i++) { var date = items.lastKey()!.add(const Duration(days: 1)); items[date] = intl.DateFormat("EEE dd MMM").format(date); } } else { for (int i = -1; i >= -3; i--) { var date = items.firstKey()!.add(const Duration(days: -1)); items[date] = intl.DateFormat("EEE dd MMM").format(date); } position += 3; } return TabState( tab: Tab(count: items.length, items: items, position: position)); } } class Tab { int count; SplayTreeMap<DateTime, String> items; int position; Tab({required this.count, required this.items, required this.position}); }
主页面代码
void main() { WidgetsFlutterBinding.ensureInitialized(); getIt.registerFactory(() => bloc_tab.TabBloc()); runApp(MultiBlocProvider( providers: [ BlocProvider(create: (context) => getIt<bloc_tab.TabBloc>()), ], child: const MyApp2(), )); } class MyApp2 extends StatefulWidget { const MyApp2({Key? key}) : super(key: key); @override State<MyApp2> createState() => _MyApp2State(); } class _MyApp2State extends State<MyApp2> { @override Widget build(BuildContext context) { return const MaterialApp( title: 'Flutter Demo', home: MyHomePage2(), ); } } class MyHomePage2 extends StatefulWidget { const MyHomePage2({Key? key}) : super(key: key); @override State<MyHomePage2> createState() => _MyHomePage2State(); } class _MyHomePage2State extends State<MyHomePage2> with TickerProviderStateMixin { late TabController _tabController; late int _currentCount; late int _currentPosition; @override void initState() { _currentPosition = BlocProvider.of<bloc_tab.TabBloc>(context).state.tab.position ?? 0; _currentCount = BlocProvider.of<bloc_tab.TabBloc>(context).state.tab.count; _tabController = TabController( length: _currentCount, vsync: this, initialIndex: _currentPosition); _tabController.addListener(onPositionChange); super.initState(); } @override Widget build(BuildContext context) { return BlocConsumer<bloc_tab.TabBloc, bloc_tab.TabState>( builder: (context, state) { return Scaffold( appBar: AppBar( title: Text("ffddfdf"), bottom: PreferredSize( preferredSize: TabBar( tabs: [],) .preferredSize, child: Material( color: Color.fromRGBO(20, 75, 102, 1), child: Column( children: [ TabBar( isScrollable: true, tabs: _getTabs(state.tab.items), controller: _tabController), Container( height: 1, decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.transparent, Colors.white54, Colors.white60, Colors.white70, Colors.white70, Colors.white70, Colors.white60, Colors.white54, Colors.transparent ], begin: Alignment.centerLeft, end: Alignment.centerRight))), ], ), )), backgroundColor: Color.fromRGBO(85, 200, 84, 1), ), body: Column(children: <Widget>[ Expanded( child: TabBarView( controller: _tabController, children: getWidgets(state.tab.items) ), ), ]), ); }, listener: (context, state) { if (_tabController.length != state.tab.count) { _tabController.removeListener(onPositionChange); _tabController.dispose(); _currentPosition = state.tab.position; if (_currentPosition > state.tab.count - 1) { _currentPosition = state.tab.count - 1; _currentPosition = _currentPosition < 0 ? 0 : _currentPosition; } _currentCount = state.tab.count; _tabController = TabController( length: state.tab.count, vsync: this, initialIndex: _currentPosition, ); _tabController.addListener(onPositionChange); } }, ); } List<Widget> _getTabs(Map<DateTime, String> items) { List<Widget> tabs = []; items.forEach((key, value) { tabs.add(Tab( text: value, )); }); return tabs; } Widget getWidget(String widgetNumber) { return Center( child: Text("Widget nr: $widgetNumber"), ); } List<Widget> getWidgets(Map<DateTime, String> items) { List<Widget> screens = []; items.forEach((key, value) { screens.add(TabPage(dateTime: key,)); }); return screens; } onPositionChange() { if (!_tabController.indexIsChanging) { _currentPosition = _tabController.index; if (_tabController.index == _tabController.length - 1) { BlocProvider.of<bloc_tab.TabBloc>(context).add(bloc_tab.TabEvent( addToEnd: true, currentPosition: _tabController.index)); } else if (_tabController.index == 0) { BlocProvider.of<bloc_tab.TabBloc>(context).add(bloc_tab.TabEvent( addToEnd: false, currentPosition: _tabController.index)); } } } } class TabPage extends StatefulWidget { DateTime dateTime; TabPage({Key? key, required this.dateTime}) : super(key: key); @override State<TabPage> createState() => _TabPageState(); } class _TabPageState extends State<TabPage> with AutomaticKeepAliveClientMixin { late Future<Fixture> futureFixture; @override void initState() { super.initState(); futureFixture = fetchFixture(widget.dateTime); } @override Widget build(BuildContext context) { super.build(context); return FutureBuilder<Fixture>( future: futureFixture, builder: (context, snapshot) { if (snapshot.hasData) { return ListView.builder( itemCount: snapshot.data!.leagues!.length, itemBuilder: (BuildContext context, int index) { return MatchItem(snapshot.data!.leagues![index].matches!.length, league: snapshot.data!.leagues![index]); }); } else if (snapshot.hasError) { return Center(child: Text("error - ${snapshot.error}")); } return const Center(child: CircularProgressIndicator()); }, ); } @override bool get wantKeepAlive { return true; } }
问题原因及解决方案
核心问题
- Tab页面缺少唯一标识Key:
TabPage未设置唯一Key,Tab列表变化时Flutter无法区分新旧页面,会错误复用原有Widget的状态,导致内容错乱。 - 状态修改违反不可变性原则:
TabState.addItem中直接修改原SplayTreeMap集合,未创建新副本,导致Bloc状态更新无法触发正确的Widget重建,页面顺序和状态出现混淆。
具体修复步骤
1. 为TabPage添加唯一Key
修改getWidgets方法,用每个Tab对应的dateTime作为唯一Key,确保Flutter能正确识别每个页面:
List<Widget> getWidgets(Map<DateTime, String> items) { List<Widget> screens = []; items.forEach((key, value) { screens.add(TabPage( key: ValueKey(key), // 添加唯一标识Key dateTime: key, )); }); return screens; }
2. 修复状态的不可变性
在TabState.addItem中创建新的SplayTreeMap副本,避免直接修改原状态集合:
TabState addItem(bool addToEnd, int currentPosition) { // 创建新集合副本,保证状态不可变 SplayTreeMap<DateTime, String> items = SplayTreeMap.from(tab.items); int position = currentPosition; if (addToEnd) { for (int i = 1; i <= 3; i++) { var date = items.lastKey()!.add(const Duration(days: 1)); items[date] = intl.DateFormat("EEE dd MMM").format(date); } } else { for (int i = -1; i >= -3; i--) { var date = items.firstKey()!.add(const Duration(days: -1)); items[date] = intl.DateFormat("EEE dd MMM").format(date); } position += 3; } return TabState( tab: Tab(count: items.length, items: items, position: position)); }
原理说明
- 唯一Key:Flutter通过Key区分不同Widget,Key相同的Widget会保留原有状态,不同Key则会重新创建。绑定
dateTime作为Key,能确保原有页面状态不会被错误复用。 - 不可变状态:Bloc要求状态不可变,直接修改原集合会导致状态未真正更新,Widget无法正确重建。创建新集合能保证状态更新触发正确的Widget重建逻辑。
内容的提问来源于stack exchange,提问作者M. Fazel
相关产品推荐
相关产品推荐

