Flutter中emit状态后BlocBuilder未刷新组件(get_it注入场景)
基于Clean Architecture开发Flutter应用时,采用get_it实现依赖注入、flutter_bloc框架做状态管理,在触发AuthBloc对应事件、调用emit方法推送新状态后,监听AuthBloc的BlocBuilder未响应状态变化,无法切换加载、欢迎、首页等对应页面组件,无法定位问题成因。相关实现代码如下:
BlocProvider配置代码
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:movies_app_leal/core/routes/routes.dart'; import 'package:movies_app_leal/features/auth/presentation/bloc/blocs.dart'; import 'package:movies_app_leal/injection_dependecy_container.dart' as di; void main() async { SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, systemNavigationBarIconBrightness: Brightness.light)); // status bar color WidgetsFlutterBinding .ensureInitialized(); // It is important to add this, to ensure not to have problems with Futures initialized the injection container await di.init(); // Initialize the dependency injection container runApp( MultiBlocProvider( providers: [ BlocProvider( create: (_) => di.sl<AuthBloc>(), ) ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Material App', routes: routes, theme: ThemeData.dark().copyWith( textTheme: ThemeData.dark().textTheme.apply( fontFamily: 'Gilroy', ), ), initialRoute: RoutesPages.loading.name, ); } }
BlocBuilder实现代码
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:movies_app_leal/features/auth/presentation/bloc/auth_bloc/auth_bloc.dart'; import 'package:movies_app_leal/features/auth/presentation/pages/welcome_page.dart'; import 'package:movies_app_leal/features/tv_show/presentation/pages/home_page.dart'; class LoadingPage extends StatelessWidget { const LoadingPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { switch (state.runtimeType) { case AuthInitialState: return const WelcomePage(); case AuthLoadingState: return const _LoadingIndicator(); case AuthLoadedState: return const HomePage(); case AuthErrorState: return const WelcomePage(); default: return const WelcomePage(); } }, ), ); } } class _LoadingIndicator extends StatelessWidget { const _LoadingIndicator({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { return SizedBox( height: MediaQuery.of(context).size.height, width: MediaQuery.of(context).size.width, child: const CircularProgressIndicator(), ); } }
依赖注入容器文件代码
import 'package:get_it/get_it.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:movies_app_leal/core/util/failure_to_message.dart'; import 'package:movies_app_leal/core/util/url_path_converter.dart'; import 'package:movies_app_leal/features/auth/data/datasources/auth_local_data_source.dart'; import 'package:movies_app_leal/features/auth/data/datasources/auth_remote_data_source.dart'; import 'package:movies_app_leal/features/auth/data/repositories/auth_repository_impl.dart'; import 'package:movies_app_leal/features/auth/domain/repositories/auth_repository.dart'; import 'package:movies_app_leal/features/auth/domain/usecases/log_out.dart'; import 'package:movies_app_leal/features/auth/domain/usecases/login.dart'; import 'package:movies_app_leal/features/auth/presentation/bloc/auth_bloc/auth_bloc.dart'; final sl = GetIt.instance; Future<void> init() async { //! Features - Auth //? Bloc sl.registerFactory(() => AuthBloc(login: sl(), logout: sl(), message: sl())); //? UseCases sl.registerLazySingleton(() => Login(sl())); sl.registerLazySingleton(() => LogOut(sl())); //? Repository sl.registerLazySingleton<AuthRepository>( () => AuthRepositoryImpl(localDataSource: sl(), remoteDataSource: sl())); //? Data Sources sl.registerLazySingleton<AuthRemoteDataSource>(() => AuthRemoteDataSourceImpl()); sl.registerLazySingleton<AuthLocalDataSource>( () => AuthLocalDataSourceImpl(sharedPreferences: sl())); //! Core sl.registerLazySingleton(() => UrlPathConverter()); sl.registerLazySingleton(() => Message()); //! External final sharedPreferences = await SharedPreferences.getInstance(); sl.registerLazySingleton(() => sharedPreferences); sl.registerLazySingleton(() => http.Client()); }
事件触发逻辑代码
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:movies_app_leal/core/routes/routes.dart'; import 'package:movies_app_leal/core/theme/theme_data.dart'; import 'package:movies_app_leal/core/widgets/movies_action_button.dart'; import 'package:movies_app_leal/core/widgets/movies_button.dart'; import 'package:movies_app_leal/core/widgets/movies_text_field.dart'; import 'package:movies_app_leal/core/widgets/movies_title.dart'; import 'package:movies_app_leal/features/auth/data/models/auth_model.dart'; import 'package:movies_app_leal/features/auth/presentation/bloc/blocs.dart'; import 'package:movies_app_leal/features/auth/presentation/widgets/backgroud_container.dart'; class LoginPage extends StatelessWidget { const LoginPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final topPadding = MediaQuery.of(context).padding.top; final authProvider = BlocProvider.of<AuthBloc>(context); return Scaffold( body: BackgroundContainer( child: Container( padding: EdgeInsets.only(top: topPadding), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const MoviesTitle(text: "Welcome!", fontSize: 25, centered: true), _LoginForm( authProvider: authProvider, ) ], ), ), ), ); } } class _LoginForm extends StatelessWidget { final AuthBloc authProvider; const _LoginForm({ Key? key, required this.authProvider, }) : super(key: key); @override Widget build(BuildContext context) { final TextEditingController nameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); void _onLogin() { final authData = AuthModel(name: nameController.text, password: passwordController.text); authProvider.add(AuthLoginEvent(auth: authData)); } void _onClose() { Navigator.pushReplacementNamed(context, RoutesPages.welcome.name); } return SingleChildScrollView( child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: black.withOpacity(0.9), borderRadius: const BorderRadius.only( topLeft: Radius.circular(40), topRight: Radius.circular(40), ), ), child: Column( children: [ MoviesActionButton( function: _onClose, icon: Icons.close_rounded, iconColor: grey, splasColor: yellow, ), MoviesTextField(controller: nameController, hintText: "Name"), MoviesTextField( controller: passwordController, hintText: "Password", obscureText: true, ), const SizedBox(height: 80), MoviesButton( text: "Log in", onPressed: _onLogin, normalButton: true, ), const SizedBox(height: 50), ], ), ), ); } }
Bloc核心逻辑代码
import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:movies_app_leal/core/usecases/usecase.dart'; import 'package:movies_app_leal/core/util/failure_to_message.dart'; import 'package:movies_app_leal/features/auth/domain/entities/auth.dart'; import 'package:movies_app_leal/features/auth/domain/usecases/log_out.dart'; import 'package:movies_app_leal/features/auth/domain/usecases/login.dart'; part 'auth_event.dart'; part 'auth_state.dart'; class AuthBloc extends Bloc<AuthEvent, AuthState> { final Login login; final LogOut logout; final Message message; AuthBloc({required this.login, required this.logout, required this.message}) : super(AuthInitialState()) { on<AuthLoginEvent>(_onLogIn); on<AuthLogoutEvent>(_onLogOut); } /// Handle the user log in. void _onLogIn(AuthLoginEvent event, Emitter<AuthState> emit) async { emit(AuthLoadingState()); // Show the loading state. final failureOrAuth = await login(Params(event.auth)); failureOrAuth.fold( (failure) => emit(AuthErrorState(message: message.mapFailureToMessage(failure))), (authData) => emit(AuthLoadedState(auth: authData))); } /// Handle the user log out. void _onLogOut(AuthLogoutEvent event, Emitter<AuthState> emit) async { emit(AuthLoadingState()); // Show the loading state. final failureOrAuth = await logout(NoParams()); failureOrAuth.fold( (failure) => emit(AuthErrorState(message: message.mapFailureToMessage(failure))), (_) => emit(AuthInitialState())); } }
问题由3个实现错误叠加导致状态变化无法驱动页面切换:
路由层级覆盖监听组件
你把BlocBuilder写在了LoadingPage内部,初始状态下BlocBuilder直接返回WelcomePage作为子组件,后续从欢迎页跳登录页时,你用Navigator.push/pushReplacement把登录页压到了新的路由栈层,完全盖住了底部的LoadingPage。这时候触发登录事件后,Bloc确实正常emit了新状态,底部LoadingPage里的BlocBuilder也确实触发了重建返回HomePage,但这个页面被压在路由栈最底部,上层盖着登录页,视觉上完全看不到切换效果。
修复方式:把BlocBuilder上移到MaterialApp的根位置,直接作为根组件根据Auth状态返回对应页面(加载页/欢迎登录相关页/首页),不要用路由压栈的方式覆盖根层的状态监听组件,所有认证相关页面切换完全由Bloc状态驱动,不要用Navigator做认证流程的页面跳转。状态判断方式不可靠
你用state.runtimeType做状态类型匹配,这种方式在Dart里遇到类继承、const构造、跨库导入等场景时很容易出现匹配失效的问题。
修复方式:改用is关键字做类型判断,参考写法:BlocBuilder<AuthBloc, AuthState>( builder: (context, state) { if (state is AuthLoadingState) return const _LoadingIndicator(); if (state is AuthLoadedState) return const HomePage(); return const WelcomePage(); }, )Bloc实例获取存在隐患
你在get_it里用registerFactory注册AuthBloc,每次调用sl<AuthBloc>()都会生成新的Bloc实例。虽然根BlocProvider的create回调只会调用一次,正常情况下通过context.read<AuthBloc>()能拿到正确的全局实例,但如果后续代码里误调用sl<AuthBloc>()获取Bloc实例并添加事件,事件会发送到完全独立的新实例上,监听旧实例的Builder自然不会响应。另外你在LoginPage里提前把Bloc实例存在widget属性里传递,也可能在组件重建时拿到旧实例。
修复方式:- 全局认证用的AuthBloc生命周期和App一致,可以把DI注册方式改成
sl.registerLazySingleton(() => AuthBloc(...)),保证全局拿到的是同一个实例; - 所有添加事件的位置不要提前存储Bloc实例,直接在回调里用
context.read<AuthBloc>().add(xxxEvent)即可。
- 全局认证用的AuthBloc生命周期和App一致,可以把DI注册方式改成
额外检查项:确认所有AuthState子类都正确继承Equatable,且把类的所有属性(比如AuthErrorState的message、AuthLoadedState的auth)都加入props列表,避免Bloc因为相等判断错误过滤掉状态更新。
内容的提问来源于stack exchange,提问作者Jozek Hajduk

