Flutter Bloc状态未更新:引导流程Loading无法转为Loaded
问题描述
我是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

