Flutter Bloc二次Emit失效:新闻详情无法显示问题求助
问题描述
使用Flutter Bloc开发新闻APP,流程为先通过外部API获取top ids,再根据每个id拉取新闻详情。获取top ids的流程正常,Bloc能正确触发emit并在界面展示结果;但获取新闻详情时,API已返回数据,Bloc的emit操作却无法让内部BlocBuilder匹配到对应状态,导致新闻详情无法显示,问题集中在第二个事件的emit逻辑和内部BlocBuilder上。
提供的代码片段
bloc file:
import 'package:news_flutter_bloc/data/model/item_model.dart'; import 'stories_event.dart'; import 'stories_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../data/api/news_api_provider.dart'; class StoriesBloc extends Bloc<StoriesEvent, StoriesState> { final NewsApiProvider _newsApiProvider = NewsApiProvider(); StoriesBloc() : super(StoriesInitialState()) { on<StoriesTopIdsEvent>((event, emit) async { try { List ids = await _newsApiProvider.fetchTopIds(); emit(StoriesTopIdsState(ids)); } catch (ex) { emit(StoriesErrorState(ex.toString())); } }); on<StoriesItemsEvent>((event, emit) async { try { ItemModel items = (await _newsApiProvider.fetchItems(event.id)); emit(StoriesItemsState(items)); } catch (ex) { emit(StoriesErrorState(ex.toString())); } }); } }
Event call file
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:news_flutter_bloc/blocs/stories_bloc.dart'; import 'package:news_flutter_bloc/blocs/stories_event.dart'; import 'package:news_flutter_bloc/blocs/stories_state.dart'; import 'package:news_flutter_bloc/data/model/item_model.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override Widget build(BuildContext context) { BlocProvider.of<StoriesBloc>(context).add(StoriesTopIdsEvent()); return Scaffold( appBar: AppBar( title: const Text( "Today's News", style: TextStyle( fontSize: 25, fontWeight: FontWeight.w300, ), ), centerTitle: true, ), body: SafeArea( child: BlocBuilder<StoriesBloc, StoriesState>( builder: (context, state) { if (state is StoriesInitialState) { return const Center(child: CircularProgressIndicator(),); } if (state is StoriesTopIdsState) { return ListView.builder( itemCount: state.ids.length, itemBuilder: (context, index) { int id = state.ids[index]; BlocProvider.of<StoriesBloc>(context).add( StoriesItemsEvent(id)); return BlocBuilder<StoriesBloc, StoriesState>( builder: (context, state) { if(state is StoriesItemsState) { ItemModel item = state.items; return Column( children: [ ListTile( title: Text(item.title.toString()), ), Divider( height: 5.0, thickness: 5.0, color: Colors.deepPurple.shade100, ), ], ); } else if(state is StoriesErrorState){ return Text(state.error); } return const Center(child: Text("2nd error")); }, ); }); }else { return const Center(child: Text('1st error')); } }, ), ), ); } }
问题根源分析
单一Bloc实例被多事件覆盖
所有列表项共用同一个StoriesBloc实例,当ListView.builder循环添加StoriesItemsEvent时,后续事件会覆盖之前的状态,导致所有内部BlocBuilder只能拿到最后一个请求的新闻数据,甚至出现状态不匹配的情况。build方法中重复触发事件
在build方法中直接调用BlocProvider.of<StoriesBloc>(context).add(StoriesTopIdsEvent()),每次界面重建都会重复发起top ids请求;同时itemBuilder每次执行也会添加StoriesItemsEvent,造成大量重复请求和状态混乱。状态未关联对应ID
StoriesItemsState仅存储新闻数据,没有绑定对应的新闻ID,内部BlocBuilder无法判断当前状态是否属于当前列表项,自然无法正确展示对应内容。
解决方案
方案一:为每个列表项创建独立Bloc实例(推荐)
让每个新闻列表项拥有自己的StoriesBloc,避免状态互相干扰:
修改HomeScreen代码
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:news_flutter_bloc/blocs/stories_bloc.dart'; import 'package:news_flutter_bloc/blocs/stories_event.dart'; import 'package:news_flutter_bloc/blocs/stories_state.dart'; import 'package:news_flutter_bloc/data/model/item_model.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { @override void initState() { super.initState(); // 仅在初始化时发起一次top ids请求 BlocProvider.of<StoriesBloc>(context).add(StoriesTopIdsEvent()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( "Today's News", style: TextStyle( fontSize: 25, fontWeight: FontWeight.w300, ), ), centerTitle: true, ), body: SafeArea( child: BlocBuilder<StoriesBloc, StoriesState>( builder: (context, state) { if (state is StoriesInitialState) { return const Center(child: CircularProgressIndicator(),); } if (state is StoriesTopIdsState) { return ListView.builder( itemCount: state.ids.length, itemBuilder: (context, index) { int id = state.ids[index]; // 为每个item创建独立的Bloc实例 return BlocProvider( create: (context) => StoriesBloc()..add(StoriesItemsEvent(id)), child: BlocBuilder<StoriesBloc, StoriesState>( builder: (context, state) { if(state is StoriesItemsState) { ItemModel item = state.items; return Column( children: [ ListTile( title: Text(item.title.toString()), ), Divider( height: 5.0, thickness: 5.0, color: Colors.deepPurple.shade100, ), ], ); } else if(state is StoriesErrorState){ return Text(state.error); } return const Center(child: CircularProgressIndicator()); }, ), ); }); }else { return const Center(child: Text('加载失败')); } }, ), ), ); } }
方案二:修改Bloc状态关联ID,过滤匹配状态
如果不想为每个item创建Bloc,可以修改状态结构,让状态携带ID,在BlocBuilder中匹配当前item的ID:
修改StoriesState(新增带ID的状态)
// 假设原StoriesItemsState修改为: class StoriesItemsState extends StoriesState { final int id; // 新增ID字段 final ItemModel items; StoriesItemsState(this.id, this.items); }
修改Bloc中的emit逻辑
on<StoriesItemsEvent>((event, emit) async { try { ItemModel items = await _newsApiProvider.fetchItems(event.id); emit(StoriesItemsState(event.id, items)); // 传入ID } catch (ex) { emit(StoriesErrorState(ex.toString())); } });
修改HomeScreen的内部BlocBuilder
// 在ListView的itemBuilder中: int id = state.ids[index]; // 仅在首次build时添加一次事件 WidgetsBinding.instance.addPostFrameCallback((_) { BlocProvider.of<StoriesBloc>(context).add(StoriesItemsEvent(id)); }); return BlocBuilder<StoriesBloc, StoriesState>( builder: (context, state) { // 仅匹配当前item的ID对应的状态 if(state is StoriesItemsState && state.id == id) { ItemModel item = state.items; return Column( children: [ ListTile( title: Text(item.title.toString()), ), Divider( height: 5.0, thickness: 5.0, color: Colors.deepPurple.shade100, ), ], ); } else if(state is StoriesErrorState){ return Text(state.error); } return const Center(child: CircularProgressIndicator()); }, );
同时记得把StoriesTopIdsEvent的添加移到initState中,避免重复请求。
额外优化建议
- 添加
AutomaticKeepAliveClientMixin到列表项,避免滑动时重建重复发起请求。 - 对API请求添加缓存逻辑,避免重复请求相同ID的新闻数据。
- 在Bloc中处理加载状态,比如新增
StoriesLoadingState,让界面显示加载指示器更合理。
内容的提问来源于stack exchange,提问作者Luchi Luchi 3

