如何基于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
相关产品推荐
相关产品推荐

