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

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个实现错误叠加导致状态变化无法驱动页面切换:

  1. 路由层级覆盖监听组件
    你把BlocBuilder写在了LoadingPage内部,初始状态下BlocBuilder直接返回WelcomePage作为子组件,后续从欢迎页跳登录页时,你用Navigator.push/pushReplacement把登录页压到了新的路由栈层,完全盖住了底部的LoadingPage。这时候触发登录事件后,Bloc确实正常emit了新状态,底部LoadingPage里的BlocBuilder也确实触发了重建返回HomePage,但这个页面被压在路由栈最底部,上层盖着登录页,视觉上完全看不到切换效果。
    修复方式:把BlocBuilder上移到MaterialApp的根位置,直接作为根组件根据Auth状态返回对应页面(加载页/欢迎登录相关页/首页),不要用路由压栈的方式覆盖根层的状态监听组件,所有认证相关页面切换完全由Bloc状态驱动,不要用Navigator做认证流程的页面跳转。

  2. 状态判断方式不可靠
    你用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();
      },
    )
    
  3. 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)即可。

额外检查项:确认所有AuthState子类都正确继承Equatable,且把类的所有属性(比如AuthErrorState的message、AuthLoadedState的auth)都加入props列表,避免Bloc因为相等判断错误过滤掉状态更新。


内容的提问来源于stack exchange,提问作者Jozek Hajduk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 03:45:31