Flutter中Firestore数据加载引发rating空值异常问题求助
我对空值检查的理解不算深入,但基本原理还是懂的😂。这次遇到空值检查异常实在搞不懂。我把数据存在textController里,用Text组件展示值,没用到TextFormField。用户通过对话框提交新数据修改,之后调用setState更新组件,本质是让用户查看资料修改,而非用表单。所以我没选FutureBuilder,因为setState会重新从Firebase拉数据覆盖修改内容。
核心问题:我有个String?类型的rating变量,在initState里通过Firestore的异步函数赋值。starting_price、description这些值都能正常显示,唯独rating出问题。一开始它是int类型,Firestore里存的是数字,后来改成String类型试了试。把rating传给组件时,因为是可空String,用空值检查运算符会触发“空值检查运算符用于空值”异常;在Text组件里传rating.toString()能正常运行,但转成double传给RatingBarIndicator(flutter_rating_bar包)时就报错。
我怀疑组件在rating赋值前就完成了构建,但明明已经在initState里初始化了。不管用不用FutureBuilder,该怎么解决?
profile_screen.dart
class ProfileScreen extends StatefulWidget { const ProfileScreen({super.key, required this.userId}); final String userId; @override State<ProfileScreen> createState() => _ProfileScreenViewState(); } class _ProfileScreenViewState extends State<ProfileScreen> { String? rating; @override void initState() { super.initState(); fetchProviderData(widget.userId); } Future<void> fetchProviderData(String userId) async { final DocumentSnapshot snapshot = await FirebaseFirestore.instance .collection('providers') .doc(userId) .get(); if (snapshot.exists) { final data = snapshot.data() as Map<String, dynamic>?; if (data != null) { setState(() { startPriceController.text = data['starting_price']; descriptionController.text = data['description']; profilePicUrl = data['profile_picture'] as String; rating = data['rating'] as String; services = data['services'] as List<dynamic>; print(rating); print(startPriceController.text); }); } } } @override Widget build(BuildContext context) { print(startPriceController.text); // IT SHOWS THE CORRECT VALUES print(descriptionController.text); print(rating); return Scaffold( body: Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(rating.toString()), RatingBarIndicator( rating: double.parse(rating.toString()), itemCount: 5, itemSize: 15, itemBuilder: (context, _) => const Icon( Icons.star, color: Colors.amber), ), ]))) ]), ], ))); } }
控制台日志
I/flutter ( 4651): LANDING: PROFILE CLICKED I/flutter ( 4651): START PRICE: I/flutter ( 4651): DESC: I/flutter ( 4651): RATING: I/flutter ( 4651): START PRICE: 20.1 I/flutter ( 4651): DESC: description here I/flutter ( 4651): RATING: 3
格式异常栈追踪
════════ Exception caught by widgets library ═══════════════════════════════════ The following FormatException was thrown building ProfileScreen(dirty, state: _ProfileScreenViewState#060dc): Invalid double The relevant error-causing widget was ProfileScreen landing_page.dart:197 When the exception was thrown, this was the stack #0 double.parse (dart:core-patch/double_patch.dart:111:28) #1 _ProfileScreenViewState.build profile_screen.dart:219 #2 StatefulElement.build framework.dart:4992 #3 ComponentElement.performRebuild framework.dart:4878 #4 StatefulElement.performRebuild framework.dart:5050 #5 Element.rebuild framework.dart:4604 #6 ComponentElement._firstBuild framework.dart:4859 #7 StatefulElement._firstBuild framework.dart:5041 #8 ComponentElement.mount framework.dart:4853 ... Normal element mounting (275 frames) #283 Element.inflateWidget framework.dart:3863 #284 MultiChildRenderObjectElement.inflateWidget framework.dart:6435 #285 Element.updateChild framework.dart:3592 #286 RenderObjectElement.updateChildren framework.dart:5964 #287 MultiChildRenderObjectElement.update framework.dart:6460 #288 Element.updateChild framework.dart:3570 #289 ComponentElement.performRebuild framework.dart:4904 #290 StatefulElement.performRebuild framework.dart:5050 #291 Element.rebuild framework.dart:4604 #292 StatefulElement.update framework.dart:5082 #293 Element.updateChild framework.dart:3570 #294 ComponentElement.performRebuild framework.dart:4904 #295 Element.rebuild framework.dart:4604 #296 ProxyElement.update framework.dart:5228 #297 Element.updateChild framework.dart:3570 #298 ComponentElement.performRebuild framework.dart:4904 #299 Element.rebuild framework.dart:4604 #300 ProxyElement.update framework.dart:5228 #301 _InheritedNotifierElement.update inherited_notifier.dart:107 #302 Element.updateChild framework.dart:3570 #303 SingleChildRenderObjectElement.update framework.dart:6307 #304 Element.updateChild framework.dart:3570 #305 ComponentElement.performRebuild framework.dart:4904 #306 StatefulElement.performRebuild framework.dart:5050 #307 Element.rebuild framework.dart:4604 #308 StatefulElement.update framework.dart:5082 #309 Element.updateChild framework.dart:3570 #310 SingleChildRenderObjectElement.update framework.dart:6307 #311 Element.updateChild framework.dart:3570 #312 SingleChildRenderObjectElement.update framework.dart:6307 #313 Element.updateChild framework.dart:3570 #314 ComponentElement.performRebuild framework.dart:4904 #315 Element.rebuild framework.dart:4604 #316 ProxyElement.update framework.dart:5228 #317 Element.updateChild framework.dart:3570 #318 ComponentElement.performRebuild framework.dart:4904 #319 StatefulElement.performRebuild framework.dart:5050 #320 Element.rebuild framework.dart:4604 #321 BuildOwner.buildScope framework.dart:2667 #322 WidgetsBinding.drawFrame binding.dart:882 #323 RendererBinding._handlePersistentFrameCallback binding.dart:378 #324 SchedulerBinding._invokeFrameCallback binding.dart:1175 #325 SchedulerBinding.handleDrawFrame binding.dart:1104 #326 SchedulerBinding._handleDrawFrame binding.dart:1015 #327 _invoke (dart:ui/hooks.dart:148:13) #328 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:318:5) #329 _drawFrame (dart:ui/hooks.dart:115:31) ════════════════════════════════════════════════════════════════════════════════
空值异常栈追踪
════════ Exception caught by widgets library ═══════════════════════════════════ The following _CastError was thrown building ProfileScreen(dirty, state: _ProfileScreenViewState#30924): Null check operator used on a null value The relevant error-causing widget was ProfileScreen landing_page.dart:197 When the exception was thrown, this was the stack #0 _ProfileScreenViewState.build profile_screen.dart:219 #1 StatefulElement.build framework.dart:4992 #2 ComponentElement.performRebuild framework.dart:4878 #3 StatefulElement.performRebuild framework.dart:5050 #4 Element.rebuild framework.dart:4604 #5 ComponentElement._firstBuild framework.dart:4859 #6 StatefulElement._firstBuild framework.dart:5041 #7 ComponentElement.mount framework.dart:4853 ... Normal element mounting (275 frames) #282 Element.inflateWidget framework.dart:3863 #283 MultiChildRenderObjectElement.inflateWidget framework.dart:6435 #284 Element.updateChild framework.dart:3592 #285 RenderObjectElement.updateChildren framework.dart:5964 #286 MultiChildRenderObjectElement.update framework.dart:6460 #287 Element.updateChild framework.dart:3570 #288 ComponentElement.performRebuild framework.dart:4904 #289 StatefulElement.performRebuild framework.dart:5050 #290 Element.rebuild framework.dart:4604 #291 StatefulElement.update framework.dart:5082 #292 Element.updateChild framework.dart:3570 #293 ComponentElement.performRebuild framework.dart:4904 #294 Element.rebuild framework.dart:4604 #295 ProxyElement.update framework.dart:5228 #296 Element.updateChild framework.dart:3570 #297 ComponentElement.performRebuild framework.dart:4904 #298 Element.rebuild framework.dart:4604 #299 ProxyElement.update framework.dart:5228 #300 _InheritedNotifierElement.update inherited_notifier.dart:107 #301 Element.updateChild framework.dart:3570 #302 SingleChildRenderObjectElement.update framework.dart:6307 #303 Element.updateChild framework.dart:3570 #304 ComponentElement.performRebuild framework.dart:4904 #305 StatefulElement.performRebuild framework.dart:5050 #306 Element.rebuild framework.dart:4604 #307 StatefulElement.update framework.dart:5082 #308 Element.updateChild framework.dart:3570 #309 SingleChildRenderObjectElement.update framework.dart:6307 #310 Element.updateChild framework.dart:3570 #311 SingleChildRenderObjectElement.update framework.dart:6307 #312 Element.updateChild framework.dart:3570 #313 ComponentElement.performRebuild framework.dart:4904 #314 Element.rebuild framework.dart:4604 #315 ProxyElement.update framework.dart:5228 #316 Element.updateChild framework.dart:3570 #317 ComponentElement.performRebuild framework.dart:4904 #318 StatefulElement.performRebuild framework.dart:5050 #319 Element.rebuild framework.dart:4604 #320 BuildOwner.buildScope framework.dart:2667 #321 WidgetsBinding.drawFrame binding.dart:882 #322 RendererBinding._handlePersistentFrameCallback binding.dart:378 #323 SchedulerBinding._invokeFrameCallback binding.dart:1175 #324 SchedulerBinding.handleDrawFrame binding.dart:1104 #325 SchedulerBinding._handleDrawFrame binding.dart:1015 #326 _invoke (dart:ui/hooks.dart:148:13) #327 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:318:5) #328 _drawFrame (dart:ui/hooks.dart:115:31) ════════════════════════════════════════════════════════════════════════════════
1. 先处理空状态再渲染RatingBar
组件第一次构建时rating是null,直接调用double.parse(rating.toString())会把null转成字符串"null",导致格式异常。先判断rating是否为空,为空时显示加载占位符:
// 替换原代码中的RatingBarIndicator rating != null ? RatingBarIndicator( rating: double.parse(rating!), itemCount: 5, itemSize: 15, itemBuilder: (context, _) => const Icon( Icons.star, color: Colors.amber), ) : const SizedBox( height: 15, child: Center(child: Text("加载中...")), )
2. 安全转换Firestore数据类型
Firestore里的rating是数字类型,直接用as String强制转换会报错,改成.toString()安全转换:
// 修改fetchProviderData里的rating赋值 rating = data['rating'].toString(); // 如果后续不需要String类型,直接存为double更省心 // double? rating; // rating = (data['rating'] as num).toDouble();
3. 用FutureBuilder优化(避免覆盖本地修改)
如果担心FutureBuilder重新拉数据覆盖修改,把本地修改状态和远程数据分开,优先用本地修改后的值:
// 添加本地修改的状态变量 String? localRating; // 用户修改时更新本地状态 void updateRating(String newRating) { setState(() { localRating = newRating; }); } // build方法中替换为FutureBuilder @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 32), child: FutureBuilder<DocumentSnapshot>( future: FirebaseFirestore.instance.collection('providers').doc(widget.userId).get(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } final data = snapshot.data?.data() as Map<String, dynamic>?; final remoteRating = data?['rating'].toString(); // 优先用本地修改的值,没有则用远程数据 final currentRating = localRating ?? remoteRating; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Flexible( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(currentRating ?? "暂无评分"), if (currentRating != null) RatingBarIndicator( rating: double.parse(currentRating), itemCount: 5, itemSize: 15, itemBuilder: (context, _) => const Icon(Icons.star, color: Colors.amber), ) else const Text("暂无评分"), ], ), ), ), ], ), ], ); }, ), ), ); }
4. 避免强制类型转换风险
所有Firestore数据转换都用安全方式,比如`data

