You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
  }
}
问题原因及解决方案

核心问题

  1. Tab页面缺少唯一标识Key:TabPage未设置唯一Key,Tab列表变化时Flutter无法区分新旧页面,会错误复用原有Widget的状态,导致内容错乱。
  2. 状态修改违反不可变性原则: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 11:15:27