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

Flutter Bloc状态未更新:引导流程Loading无法转为Loaded

Flutter Bloc引导流程状态切换失败排查

问题描述

我是Flutter Bloc新手,正在用Bloc实现用户输入数据的创建、读取、编辑和存储功能,用于应用引导流程。用户通过Firebase邮箱密码认证后,本该进入引导问卷,但Bloc状态一直卡在OnboardingLoading,无法切换到OnboardingLoaded,界面始终显示圆形进度条,没法展示后续表单内容。参考多个视频教程没解决,附上代码求技术指导。


相关代码

Main.dart

int? isViewed;
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  await FirebaseAppCheck.instance.activate();
  
  final prefs = await SharedPreferences.getInstance();
  final showLogin = prefs.getBool('showLogin') ?? false;
  Paint.enableDithering = true;
  
// This is for our onboarding screen
isViewed = prefs.getInt('onboard');

  runApp(MyApp(showLogin: showLogin));
}

class MyApp extends StatelessWidget {
  final bool showLogin;
  
  const MyApp({Key? key,
  required this.showLogin}) : super(key: key);


  @override
  Widget build(BuildContext context) {
    return MultiRepositoryProvider(
      providers: [
        RepositoryProvider(
          create: (context) => DatabaseRepository()
          ),
          RepositoryProvider(
            create: (context) => StorageRepository()
            )
      ],
      child: MultiBlocProvider(
        providers: [
          BlocProvider<OnboardingBloc>(
            create: (context) => OnboardingBloc(
              databaseRepository: context.read<DatabaseRepository>(),
              storageRepository: context.read<StorageRepository>()
            ))
        ],
          child: MaterialApp(
            title: 'Strength',
            debugShowCheckedModeBanner: false,
            initialRoute: AccountOnboarding.routeName, // Splash Screen
            routes: {
              '/splash screen' : (context) => const SplashScreen(),
              '/main onboarding' : (context) => const OnboardingScreen(),
              '/landing' : (context) => LandingScreen(),
              '/login' : (context) => const LoginScreen2(),
              '/dashboard' : (context) => const Dashboard(),
              '/profile onboarding' : (context) => const AccountOnboarding()
            },
            
            home: AccountOnboarding() // FINAL SCREEN IS SPLASH SCREEN
          )),
    );
      }
  }

Onboarding Bloc

part 'onboarding_event.dart';
part 'onboarding_state.dart';

class OnboardingBloc extends Bloc<OnboardingEvent, OnboardingState> {
  final DatabaseRepository _databaseRepository;
  final StorageRepository _storageRepository;

  OnboardingBloc({
    required DatabaseRepository databaseRepository,
    required StorageRepository storageRepository,
  }) : 
  _databaseRepository = databaseRepository,
  _storageRepository = storageRepository,
  super(OnboardingLoading()) {
    on<StartOnboarding>(_onStartOnboarding);
    on<UpdateUser>(_onUpdateUser);
    on<UpdateUserImage>(_onUpdateUserImage);
  }

  void _onStartOnboarding(
    StartOnboarding event, 
    Emitter<OnboardingState> emit) 
    async {
      await _databaseRepository.createUser(event.user);
      emit(OnboardingLoaded(user: event.user));
    }

  void _onUpdateUser(
    UpdateUser event, 
    Emitter<OnboardingState> emit) {
      if (state is OnboardingLoaded) {
        _databaseRepository.UpdateUser(event.user);
        emit(OnboardingLoaded(user: event.user));
      }
    }

  void _onUpdateUserImage(
    UpdateUserImage event, 
    Emitter<OnboardingState> emit) 
    async{
      if (state is OnboardingLoaded) {
        User user = (state as OnboardingLoaded).user;

        await _storageRepository.uploadImage(user, event.image);

        _databaseRepository.getUser(user.id!).listen((user) {
          add(UpdateUser(user: user));
        });
      }
    }
}

Onboarding States

part of 'onboarding_bloc.dart';

abstract class OnboardingState extends Equatable {
  const OnboardingState();

  @override
  List<Object> get props => [];
}

class OnboardingLoading extends OnboardingState {}

class OnboardingLoaded extends OnboardingState {
  final User user;

  OnboardingLoaded({required this.user});

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

Onboarding Screen (AccountOnboarding)

class AccountOnboarding extends StatelessWidget {
  static const String routeName = '/profile onboarding';

  static Route route() {
    return MaterialPageRoute(
      settings: RouteSettings(name: routeName),
      builder: (context) => MultiBlocProvider(
        providers: [
          BlocProvider<OnboardingBloc>
          (create: (_) => OnboardingBloc(
            databaseRepository: context.read<DatabaseRepository>(), 
            storageRepository: context.read<StorageRepository>(),
            )..add(StartOnboarding())
            )
        ],
        child: AccountOnboarding(),
      ));
  }

    static const List<Tab> tabs = <Tab>[
    Tab(text: 'Name'),
    Tab(text: 'Age and Profile'),
    Tab(text: 'Bio and Interests'),
    Tab(text: 'Selection')
  ];

  @override
  Widget build(BuildContext context) {

    return DefaultTabController(
      length: tabs.length,
       child: Builder(builder: (BuildContext context) {
        final TabController tabController = DefaultTabController.of(context)!;
        tabController.addListener(() {
          if (!tabController.indexIsChanging) {}
        });
        return Scaffold(
          resizeToAvoidBottomInset: false,
          backgroundColor: const Color(0xff31708c),
          appBar: AppBar(
            automaticallyImplyLeading: false,
            backgroundColor: Colors.transparent,
            elevation: 0,
            title: Row(
              children: [
                Expanded(
                  child: Image.asset('assets/images/Logo_Strength.png',
                  height: 50),
                ),
                Expanded(
                  flex: 2,
                  child: RichText(
                              text: TextSpan(
                                style: GoogleFonts.montserrat(
                                  fontSize: 30),
                                  children: <TextSpan> [
                                    TextSpan(text: 'Stren', 
                                    style: GoogleFonts.montserrat(
                                      color: Colors.white, 
                                      fontWeight: FontWeight.bold, 
                                      letterSpacing: 1,
                                      shadows: [
                                        Shadow(
                                          color: Colors.black.withOpacity(0.7),
                                          offset: const Offset(1.5, 0.0))
                                      ])),
                
                                    TextSpan(text: ';', 
                                    style: GoogleFonts.montserrat(
                                      color: const Color(0xffef6a7a), fontWeight: FontWeight.bold, 
                                      letterSpacing: 1,
                                      shadows: [
                                        Shadow(
                                          color: Colors.black.withOpacity(0.7),
                                          offset: const Offset(1.5, 0.0))
                                      ])),
                
                                    TextSpan(text: 'th', 
                                    style: GoogleFonts.montserrat(
                                      color: Colors.white, 
                                      fontWeight: FontWeight.bold, 
                                      letterSpacing: 1,
                                      shadows: [
                                        Shadow(
                                          color: Colors.black.withOpacity(0.7),
                                          offset: const Offset(1.5, 0.0))
                                      ]))
                          ],
                        ),
                      ),
                ),
              ],
            )
        ),
        body: TabBarView(
              // physics: const NeverScrollableScrollPhysics(),
              children: [
                NamePage(tabController: tabController,),
                ageAndPicture(tabController: tabController,),
                bioAndInterests(tabController: tabController,),
                SelectionPage(tabController: tabController,)
              ],
            ));
            }
          ));}}

引导首屏 (NamePage)

class NamePage extends StatelessWidget {
  final TabController tabController;

  const NamePage({Key? key,
  required this.tabController}) 
  : super(key: key);

  @override

  Widget build(BuildContext context) {
    double _height = MediaQuery.of(context).size.height;

    return BlocBuilder<OnboardingBloc, OnboardingState>(
      builder: (context, state) {
        if (state is OnboardingLoading) {
          return Center(
            child: CircularProgressIndicator(),
          );
        }

        if (state is OnboardingLoaded) {
        return Scaffold(
            resizeToAvoidBottomInset: false,
            backgroundColor: const Color(0xff31708c),
        
          body: Padding(
            padding: EdgeInsets.only(
              left: 30,
              right: 30,
              top: _height * 0.055,
              bottom: _height * 0.05),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Column(
                    children: [
                      Column(
                        children: <Widget>[
                          Text('Here at Strength, we respect your preferences. How would you like to be addressed?',
                          style: GoogleFonts.montserrat(
                            color: Colors.white,
                            fontSize: 19,
                            fontWeight: FontWeight.w600
                          ),
                          textAlign: TextAlign.center,),
                          Padding(
                            padding: EdgeInsets.only(
                              top: _height * 0.22),
                              child: SingleChildScrollView(
                                child: Column(
                                  crossAxisAlignment: CrossAxisAlignment.start,
                                  children: <Widget>[
                                    TextField(
                                      maxLength: 14,
                                      maxLengthEnforcement: MaxLengthEnforcement.enforced,
                                      onChanged: (value) {
                                        context.read<OnboardingBloc>()
                                        .add(UpdateUser(
                                          user: state.user.copyWith(name: value)));
                                      },
                                      cursorColor: Colors.white,
                                      style: GoogleFonts.montserrat(
                                        color: Colors.white,
                                        fontSize: 19,
                                        height: 2
                                      ),
                                      decoration: InputDecoration(
                                        enabledBorder: InputBorder.none,
                                        disabledBorder: InputBorder.none,
                                        focusedBorder: InputBorder.none,
                                        filled: true,
                                        helperText: 'Your name',
                                        helperStyle: GoogleFonts.montserrat(
                                          color: Colors.white,
                                          fontSize: 14.5,
                                          fontWeight: FontWeight.w600
                                        ),
                                        counterStyle: GoogleFonts.montserrat(
                                          fontSize: 14.5,
                                          fontWeight: FontWeight.w600
                                        ),
                                        // counterText: "",
                                        labelStyle: 
                                        GoogleFonts.montserrat(
                                          color: Colors.white,
                                          fontSize: 18),
                                        hintText: 'Please call me . . .',
                                        hintStyle: GoogleFonts.montserrat(
                                          color: Colors.white54,
                                          fontWeight: FontWeight.w600,
                                          fontSize: 18),
                                          border: InputBorder.none),
                                    ),
                                  ],
                                ),
                              ),),
                        ],
                      ),
                    ],
                  ),
                  Column(
                    children: [
                      Align(
                        alignment: 
                        Alignment.topLeft,
                        child: Text('1/4',
                        style: GoogleFonts.montserrat(
                          color: Colors.white,
                          fontWeight: FontWeight.w500
                        ),)),
                      const SizedBox(height: 5,),
                      const StepProgressIndicator(
                        totalSteps: 4, 
                        currentStep: 1,
                        roundedEdges: Radius.circular(20),
                        size: 3,
                        padding: 3,
                        selectedColor: Colors.white,
                        unselectedColor: Color.fromARGB(255, 20, 83, 106),),
                        const SizedBox(height: 13,),
                        CustomButton(tabController: tabController)
                    ],
                  ),
                ],
              ),
            ),
        );
      }
      else {
        return const Text('Something went wrong.');
      }
      }
    );
}
}

问题排查与解决方案

1. StartOnboarding事件参数缺失

在AccountOnboarding.route()中,你调用..add(StartOnboarding())时没有传入User对象,但_onStartOnboarding方法需要用event.user执行数据库创建操作。这会导致数据库调用失败或卡住,无法触发emit(OnboardingLoaded)。

修复:
从Firebase Auth获取当前已认证用户,构建自定义User实例后再发送事件:

static Route route() {
  return MaterialPageRoute(
    settings: RouteSettings(name: routeName),
    builder: (context) {
      final firebaseUser = FirebaseAuth.instance.currentUser;
      if (firebaseUser == null) {
        // 处理未登录情况,比如跳转登录页
        Navigator.pushReplacementNamed(context, '/login');
        return const SizedBox.shrink();
      }
      // 初始化自定义User对象
      final initialUser = User(
        id: firebaseUser.uid,
        email: firebaseUser.email,
      );
      context.read<OnboardingBloc>().add(StartOnboarding(user: initialUser));
      return AccountOnboarding();
    },
  );
}

2. 重复创建Bloc实例

MyApp中已经通过MultiBlocProvider提供了OnboardingBloc,但AccountOnboarding.route()又重新创建了一个新实例。NamePage监听的是上层的Bloc,而事件发送给了新Bloc,状态更新无法同步,导致界面一直显示加载。

修复:
移除AccountOnboarding.route()中的MultiBlocProvider,直接使用上层提供的Bloc发送事件(代码同上)。

3. 异常捕获缺失

_onStartOnboarding中没有异常处理,如果createUser抛出网络或权限异常,Bloc会卡住,无法发出新状态。

修复:
添加try-catch块处理异常:

void _onStartOnboarding(
  StartOnboarding event, 
  Emitter<OnboardingState> emit) 
  async {
    try {
      await _databaseRepository.createUser(event.user);
      emit(OnboardingLoaded(user: event.user));
    } catch (e) {
      print("创建用户失败: $e");
      // 可选:定义OnboardingError状态并发出,方便界面展示错误
      // emit(OnboardingError(message: e.toString()));
    }
}

4. 验证User类的Equatable实现

确保自定义User类正确继承Equatable并覆盖props,否则OnboardingLoaded状态的变化可能无法被BlocBuilder识别:

class User extends Equatable {
  final String id;
  final String? name;
  final String? email;

  const User({required this.id, this.name, this.email});

  User copyWith({String? name, String? email}) {
    return User(
      id: id,
      name: name ?? this.name,
      email: email ?? this.email,
    );
  }

  @override
  List<Object?> get props => [id, name, email];
}

内容的提问来源于stack exchange,提问作者Ta-Ty

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 19:15:43