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

如何基于Bloc从SharedPreferences加载的主题更新TabController长度

问题描述

我有一个Home Bloc,用于从preferences包中加载List<String>,之后希望基于该列表更新TabBar和TabBarView,因此需要先设置TabController的长度,但不清楚具体实现方式。目前代码运行正常,但使用context.watch时出现错误(错误提示:setState() or markNeedsBuild() called during build.,原因是在initState中调用状态监听方法,此时Widget还未完成挂载)。

以下是我的代码:

HomeDataState

abstract class HomeDataState {}

class HomeDataInitialState extends HomeDataState{}

class HomeDataLoadingState extends HomeDataState {}

class HomeDataLoadedState<T> extends HomeDataState {
  final T data;
  HomeDataLoadedState({required this.data});
}

class HomeDataErrorState extends HomeDataState {
  final String error;

  HomeDataErrorState({required this.error});
} 

BLOC

Future<void> _loadAllFeed(
  HomeLoadAllFeed event,
  Emitter<HomeBlocState> emit,
) async {
  log('loading home feed..');

  /// emit loading all data
  emit(state.copyWith(
      newHeadLineState: HomeDataLoadingState(),
      newTopicDataState: HomeDataLoadingState()));

  /// fetches the headline news for country
  final headLineResponse = await _newsRepository.fetchHeadLineNews();

  /// gets the first topic from selected list
  final List<String>? userTopics = await _prefOperator.getUserTopics();
  topics = userTopics??[];

  /// gets the news based on the topic
  final firstTopicNews =
      await _newsRepository.getNewsBaseOnTopic(userTopics?.first ?? 'Car');

  /// if states was success for both
  if (headLineResponse is DataSuccess && firstTopicNews is DataSuccess) {
    emit(
      state.copyWith(
          newHeadLineState: HomeDataLoadedState(data: headLineResponse.data),
          newTopicDataState: HomeDataLoadedState(data: firstTopicNews.data),
          newUserTopicDataState: HomeDataLoadedState(data: userTopics)),
    );
  }
}
解决方案

1. 错误原因

在initState中调用context.watch<HomeBloc>()会触发报错,因为此时Widget还未完成挂载,BuildContext还不能用于监听状态变化,会导致setState()在build过程中被调用的冲突。

2. 正确实现步骤

方式一:使用BlocListener监听状态,动态初始化TabController

在页面中使用BlocListener监听HomeBloc的状态,当newUserTopicDataState变为HomeDataLoadedState时,获取到userTopics列表,再初始化或更新TabController:

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  List<String> _userTopics = [];

  @override
  void initState() {
    super.initState();
    // 先初始化默认长度的控制器,后续更新
    _tabController = TabController(length: 0, vsync: this);
    // 触发Bloc加载数据
    context.read<HomeBloc>().add(HomeLoadAllFeed());
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: BlocListener<HomeBloc, HomeBlocState>(
          listener: (context, state) {
            final topicState = state.newUserTopicDataState;
            if (topicState is HomeDataLoadedState<List<String>?>) {
              final newTopics = topicState.data ?? [];
              if (newTopics != _userTopics) {
                setState(() {
                  _userTopics = newTopics;
                  // 更新TabController长度,处理索引越界
                  _tabController = TabController(
                    length: _userTopics.length,
                    vsync: this,
                    initialIndex: _tabController.index.clamp(0, newTopics.length - 1),
                  );
                });
              }
            }
          },
          child: _userTopics.isEmpty 
              ? const LinearProgressIndicator() // 加载时显示进度条
              : TabBar(
                  controller: _tabController,
                  tabs: _userTopics.map((topic) => Tab(text: topic)).toList(),
                ),
        ),
      ),
      body: BlocBuilder<HomeBloc, HomeBlocState>(
        builder: (context, state) {
          final topicState = state.newUserTopicDataState;
          if (topicState is HomeDataLoadingState) {
            return const Center(child: CircularProgressIndicator());
          }
          if (topicState is HomeDataErrorState) {
            return Center(child: Text(topicState.error));
          }
          return TabBarView(
            controller: _tabController,
            children: _userTopics.map((topic) {
              // 根据topic构建对应的页面,比如调用Bloc获取该topic的新闻
              return TopicNewsPage(topic: topic);
            }).toList(),
          );
        },
      ),
    );
  }
}

方式二:使用FutureBuilder直接获取偏好设置(适合不需要Bloc其他状态的场景)

如果TabController只依赖preferences中的userTopics,可以直接用FutureBuilder在build方法中获取数据,简化实现:

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  late Future<List<String>> _getUserTopicsFuture;

  @override
  void initState() {
    super.initState();
    _getUserTopicsFuture = PrefOperator().getUserTopics().then((value) => value ?? []);
    // 同时触发Bloc加载其他数据
    context.read<HomeBloc>().add(HomeLoadAllFeed());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: FutureBuilder<List<String>>(
          future: _getUserTopicsFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const LinearProgressIndicator();
            }
            final userTopics = snapshot.data ?? [];
            return TabBar(
              controller: TabController(length: userTopics.length, vsync: this),
              tabs: userTopics.map((topic) => Tab(text: topic)).toList(),
            );
          },
        ),
      ),
      body: FutureBuilder<List<String>>(
        future: _getUserTopicsFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          final userTopics = snapshot.data ?? [];
          return TabBarView(
            controller: TabController(length: userTopics.length, vsync: this),
            children: userTopics.map((topic) {
              return TopicNewsPage(topic: topic);
            }).toList(),
          );
        },
      ),
    );
  }
}

注意事项

  • 必须混入SingleTickerProviderStateMixin,为TabController提供vsync参数,避免内存泄漏
  • 更新TabController时,要处理初始索引,防止索引越界
  • 记得在dispose方法中销毁TabController

内容的提问来源于stack exchange,提问作者Abolfazl Mashhadi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 22:54:58