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

Flutter Bloc Listener未随状态变更隐藏加载弹窗问题

Flutter Bloc Consumer Listener未响应:登录后加载弹窗无法隐藏

问题场景

登录页点击登录按钮后,加载弹窗正常显示;登录成功进入NotesView后,加载弹窗未自动隐藏,仅热重启后才消失。尝试以下方案均无效:

  • 在AuthStateLoggedOut的Equatable props中添加loadingText
  • 在NotesView的initState中调用LoadingScreen().hide()
  • 在BlocConsumer的builder中判断AuthStateLoggedIn状态时调用hide()

核心代码片段

Main.dart

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    const CustomTheme(
      initialThemeKey: ThemeKeys.light,
      child: HomePage(),
    ),
  );
}

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Von Note',
      theme: CustomTheme.of(context),
      routes: {
        createUpdateNoteView: (context) => const CreateUpdateNoteView(),
      },
      home: BlocProvider<AuthBloc>(
          create: (context) => AuthBloc(FirebaseAuthProvider()),
          child: const Content()),
    );
  }
}

class Content extends StatelessWidget {
  const Content({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    context.read<AuthBloc>().add(const AuthEventInitialize());
    return BlocConsumer<AuthBloc, AuthState>(
      listener: (context, state) {
        if (state.isLoading) {
          LoadingScreen().show(
            context: context,
            text: state.loadingText.toString(),
          );
        } else {
          LoadingScreen().hide();
        }
      },
      builder: (context, state) {
        if (state is AuthStateLoggedIn) {
          return const NotesView();
        } else if (state is AuthStateNeedVerification) {
          return const VerifyEmailView();
        } else if (state is AuthStateLoggedOut) {
          return const LoginView();
        } else if (state is AuthStateRegistering) {
          return const RegisterView();
        } else {
          return const Scaffold(
            body: Splash(),
          );
        }
      },
    );
  }
}

AuthBloc登录事件处理

on<AuthEventLogIn>((event, emit) async {
      emit(const AuthStateLoggedOut(
        exception: null,
        isLoading: true,
        loadingText: 'Please wait while I log you in',
      ));
      final email = event.email;
      final password = event.password;
      try {
        final user = await provider.logIn(
          email: email,
          password: password,
        );
        if (user.isEmailVerified) {
          emit(const AuthStateLoggedOut(
            exception: null,
            isLoading: false,
          ));
          emit(AuthStateLoggedIn(
            user: user,
            isLoading: false,
          ));
        } else {
          emit(
            const AuthStateLoggedOut(
              exception: null,
              isLoading: false,
            ),
          );
          emit(const AuthStateNeedVerification(isLoading: false));
        }
      } on Exception catch (e) {
        emit(AuthStateLoggedOut(
          exception: e,
          isLoading: false,
        ));
      }
    });

AuthStateLoggedOut

class AuthStateLoggedOut extends AuthState with EquatableMixin {
  final Exception? exception;
  const AuthStateLoggedOut({
    required this.exception,
    required bool isLoading,
    String? loadingText,
  }) : super(
          isLoading: isLoading,
          loadingText: loadingText,
        );

  @override
  List<Object?> get props => [exception, isLoading];
}

问题分析

  1. 冗余状态切换引发Context异常:登录成功后先发射AuthStateLoggedOut(isLoading: false),再切换到AuthStateLoggedIn,导致BlocConsumer的builder短暂渲染LoginView,原listener的Context因Widget树临时变化,后续AuthStateLoggedIn的隐藏操作失效。
  2. LoadingScreen实现缺陷:若未通过单例持有OverlayEntry引用,每次调用LoadingScreen()都会创建新实例,hide操作无法定位到之前显示的弹窗。
  3. 重复初始化事件:Content为StatelessWidget,build方法可能多次执行,导致重复添加AuthEventInitialize,引发状态混乱。

解决方案

1. 优化Bloc状态发射逻辑

移除冗余的AuthStateLoggedOut(isLoading: false),直接切换到目标状态:

on<AuthEventLogIn>((event, emit) async {
  emit(const AuthStateLoggedOut(
    exception: null,
    isLoading: true,
    loadingText: 'Please wait while I log you in',
  ));
  final email = event.email;
  final password = event.password;
  try {
    final user = await provider.logIn(
      email: email,
      password: password,
    );
    if (user.isEmailVerified) {
      // 直接发射登录成功状态
      emit(AuthStateLoggedIn(
        user: user,
        isLoading: false,
      ));
    } else {
      emit(const AuthStateNeedVerification(isLoading: false));
    }
  } on Exception catch (e) {
    emit(AuthStateLoggedOut(
      exception: e,
      isLoading: false,
    ));
  }
});

2. 修复LoadingScreen单例实现

通过单例持有OverlayEntry,保证show和hide操作对应同一弹窗:

class LoadingScreen {
  static LoadingScreen? _instance;
  OverlayEntry? _overlayEntry;

  LoadingScreen._internal();

  factory LoadingScreen() {
    _instance ??= LoadingScreen._internal();
    return _instance!;
  }

  void show({required BuildContext context, required String text}) {
    if (_overlayEntry != null) return;

    _overlayEntry = OverlayEntry(
      builder: (context) => Container(
        color: Colors.black54,
        child: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const CircularProgressIndicator(),
              const SizedBox(height: 16),
              Text(text),
            ],
          ),
        ),
      ),
    );

    Overlay.of(context).insert(_overlayEntry!);
  }

  void hide() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }
}

3. 将Content改为StatefulWidget避免重复初始化

在initState中仅触发一次初始化事件:

class Content extends StatefulWidget {
  const Content({Key? key}) : super(key: key);

  @override
  State<Content> createState() => _ContentState();
}

class _ContentState extends State<Content> {
  @override
  void initState() {
    super.initState();
    // 仅初始化时触发一次
    context.read<AuthBloc>().add(const AuthEventInitialize());
  }

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<AuthBloc, AuthState>(
      listener: (context, state) {
        print('当前状态: ${state.runtimeType}, isLoading: ${state.isLoading}'); // 调试日志
        if (state.isLoading) {
          LoadingScreen().show(
            context: context,
            text: state.loadingText ?? '加载中...',
          );
        } else {
          LoadingScreen().hide();
        }
      },
      builder: (context, state) {
        if (state is AuthStateLoggedIn) {
          return const NotesView();
        } else if (state is AuthStateNeedVerification) {
          return const VerifyEmailView();
        } else if (state is AuthStateLoggedOut) {
          return const LoginView();
        } else if (state is AuthStateRegistering) {
          return const RegisterView();
        } else {
          return const Scaffold(body: Splash());
        }
      },
    );
  }
}

4. 确保AuthStateLoggedIn的Equatable配置

检查AuthStateLoggedIn是否正确继承AuthState并将isLoading加入props:

class AuthStateLoggedIn extends AuthState with EquatableMixin {
  final User user;
  const AuthStateLoggedIn({
    required this.user,
    required bool isLoading,
  }) : super(isLoading: isLoading);

  @override
  List<Object?> get props => [user, isLoading];
}

验证方法

在BlocConsumer的listener中添加打印日志,确认所有状态变化均被正确接收,且isLoading值符合预期。

内容的提问来源于stack exchange,提问作者Ali Mahfud

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 00:55:30