使用FutureBuilder加载URL渲染CircleAvatar时应用崩溃问题排查
问题分析:Firebase Storage获取URL导致类型转换崩溃
我想通过获取的图片URL渲染CircleAvatar,计划将其backgroundImage设置为基于该URL的CachedNetworkImageProvider。为此实现了从Firebase Storage获取下载URL的fetchProfileImageUrl方法,并用FutureBuilder构建UI,但应用运行时崩溃,抛出**“type 'Null' is not a subtype of type 'String' in type cast”**类型转换错误。
获取URL的代码
Future<String?> fetchProfileImageUrl() async { String uid = auth.currentUser?.uid ?? ""; if (uid.isEmpty){ return null; } final profileImageRef = storage.ref().child("/users/$uid/$uid"); try { final String downloadUrl = await profileImageRef.getDownloadURL(); print("Got download URL"); return downloadUrl; } on FirebaseException catch (e) { print("Could not load image: $e"); } }
FutureBuilder构建代码
@override Widget build(BuildContext context) { return FutureBuilder<dynamic>( future: storage.fetchProfileImageUrl(), builder: (BuildContext context, AsyncSnapshot<dynamic> imageUrl) { // print("IMAGE URL: ${imageUrl.data as String}"); return CachedNetworkImage( imageUrl: imageUrl.data as String ?? "https://via.placeholder.com/150", imageBuilder: (context, imageProvider) => Container( child: uploadingPhoto ? SpinKitDualRing(color: primaryColor) : Column( children: [ Container( width: 140, height: 120, child: Stack( children: [ CircleAvatar( radius: 50, backgroundColor: Theme.of(context).primaryColor, child: CircleAvatar( radius: 48, backgroundColor: Colors.white, backgroundImage: CachedNetworkImageProvider(imageUrl.data as String) // backgroundImage: (profilePictureUrl != null) ? profilePicture?.image : AssetImage("assets/images/blank_background.png"), ), ), Positioned( bottom: 5, right: 25, child: Container( decoration: BoxDecoration(shape: BoxShape.circle, color: secondaryColor), child: Center( child: IconButton( icon: Icon(Icons.photo_camera, size: 25), color: Colors.white, onPressed: () async { final pickedImage = await picker.pickImage(source: ImageSource.camera, imageQuality: 25, preferredCameraDevice: CameraDevice.front); // final photos = await FilePicker.platform.pickFiles(allowMultiple: false, type: FileType.image); if (pickedImage == null){ ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("No photo picked.")) ); } else { setState(() { uploadingPhoto = true; }); final path = pickedImage.path; final filename = pickedImage.name; print(path); print(filename); if (path != null) { await storage.uploadPhoto(path, filename).then((value) { setState(() { uploadingPhoto = false; }); print("successfully uploaded"); }); } } }, ), ), ), ) ] ), ), Padding( padding: EdgeInsets.all(20.0), child: Text("anonymous", style: TextStyle(fontSize: 24),), ) ], ), ), placeholder: (context, url) => CircularProgressIndicator(), errorWidget: (context, url, error) => Icon(Icons.error), ); } ); }
报错信息
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ The following _CastError was thrown building FutureBuilder<dynamic>(dirty, state: _FutureBuilderState<dynamic>#49de6): type 'Null' is not a subtype of type 'String' in type cast The relevant error-causing widget was: FutureBuilder<dynamic> FutureBuilder:file:///Users/zorgan/Documents/test2022/lib/pages/profile.dart:49:12 When the exception was thrown, this was the stack: #0 _ProfileState.build.<anonymous closure> (package:test2022/pages/profile.dart:54:35) #1 _FutureBuilderState.build (package:flutter/src/widgets/async.dart:616:55) #2 StatefulElement.build (package:flutter/src/widgets/framework.dart:4992:27) #3 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4878:15) #4 StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5050:11) #5 Element.rebuild (package:flutter/src/widgets/framework.dart:4604:5) #6 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4859:5) #7 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:5041:11) #8 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4853:5) ... Normal element mounting (61 frames) #69 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3863:16) #70 Element.updateChild (package:flutter/src/widgets/framework.dart:3592:18) #71 SliverMultiBoxAdaptorElement.updateChild (package:flutter/src/widgets/sliver.dart:1405:37) #72 SliverMultiBoxAdaptorElement.createChild.<anonymous closure> (package:flutter/src/widgets/sliver.dart:1390:20) #73 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2605:19) #74 SliverMultiBoxAdaptorElement.createChild (package:flutter/src/widgets/sliver.dart:1382:12) #75 RenderSliverMultiBoxAdaptor._createOrObtainChild.<anonymous closure> (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:353:23) #76 RenderObject.invokeLayoutCallback.<anonymous closure> (package:flutter/src/rendering/object.dart:2246:59) #77 PipelineOwner._enableMutationsToDirtySubtrees (package:flutter/src/rendering/object.dart:1035:15) #78 RenderObject.invokeLayoutCallback (package:flutter/src/rendering/object.dart:2246:14) #79 RenderSliverMultiBoxAdaptor._createOrObtainChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:342:5) #80 RenderSliverMultiBoxAdaptor.addInitialChild (package:flutter/src/rendering/sliver_multi_box_adaptor.dart:428:5) #81 RenderSliverList.performLayout (package:flutter/src/rendering/sliver_list.dart:79:12) #82 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #83 RenderSliverEdgeInsetsPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:149:12) #84 RenderSliverPadding.performLayout (package:flutter/src/rendering/sliver_padding.dart:386:11) #85 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #86 RenderViewportBase.layoutChildSequence (package:flutter/src/rendering/viewport.dart:516:13) #87 RenderViewport._attemptLayout (package:flutter/src/rendering/viewport.dart:1600:12) #88 RenderViewport.performLayout (package:flutter/src/rendering/viewport.dart:1507:20) #89 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #90 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #91 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #92 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #93 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #94 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #95 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #96 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #97 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #98 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #99 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #100 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #101 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #102 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #103 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #104 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #105 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #106 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #107 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #108 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #109 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #110 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #111 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #112 RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:120:14) #113 _RenderCustomClip.performLayout (package:flutter/src/rendering/proxy_box.dart:1467:11) #114 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #115 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #116 RenderPositionedBox.performLayout (package:flutter/src/rendering/shifted_box.dart:451:14) #117 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #118 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #119 RenderPadding.performLayout (package:flutter/src/rendering/shifted_box.dart:249:12) #120 RenderObject.layout (package:flutter/src/rendering/object.dart:2135:7) #121 RenderBox.layout (package:flutter/src/rendering/box.dart:2418:11) #122 MultiChildLayoutDelegate.layoutChild (package:flutter/src/rendering/custom_layout.dart:171:12) #123 _ScaffoldLayout.performLayout (package:flutter/src/material/scaffold.dart:1055:7) #124 MultiChildLayoutDelegate._callPerformLayout (package:flutter/src/rendering/custom_layout.dart:240:7) #125 RenderCustomMultiChildLayoutBox.performLayout (package:flutter/src/rendering/custom_layout.dart:410:14) #126 RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:1973:7) #127 PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:999:18) #128 RendererBinding.drawFrame (package:flutter/src/rendering/binding.dart:513:19) #129 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:884:13) #130 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:378:5) #131 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1175:15) #132 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1104:9) #133 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1015:5) #134 _invoke (dart:ui/hooks.dart:148:13) #135 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:318:5) #136 _drawFrame (dart:ui/hooks.dart:115:31)
问题根源
- 强制类型转换顺序错误:
imageUrl.data as String ?? "https://via.placeholder.com/150"中,as String优先执行,当imageUrl.data为null时,强制转换直接抛出类型错误,??运算符无法生效。 - 未处理Future异步状态:代码直接使用
imageUrl.data,未判断Future的加载状态(如ConnectionState.waiting),加载过程中imageUrl.data为null,触发强制转换崩溃。 - fetchProfileImageUrl存在隐式返回null:捕获FirebaseException后未明确返回值,Dart默认返回null,导致
imageUrl.data为null时触发转换错误。
修复方案
1. 修正类型转换逻辑
将强制转换放到??之后,先处理null情况:
imageUrl: (imageUrl.data as String?) ?? "https://via.placeholder.com/150", backgroundImage: CachedNetworkImageProvider((imageUrl.data as String?) ?? "https://via.placeholder.com/150")
2. 处理Future的不同状态
在builder中先判断连接状态,避免加载中或出错时访问null数据:
builder: (BuildContext context, AsyncSnapshot<dynamic> imageUrl) { if (imageUrl.connectionState == ConnectionState.waiting) { return SpinKitDualRing(color: primaryColor); } if (imageUrl.hasError || imageUrl.data == null) { return CircleAvatar( radius: 48, backgroundColor: Colors.white, backgroundImage: AssetImage("assets/images/blank_background.png"), ); } final String url = imageUrl.data as String; return CachedNetworkImage( imageUrl: url, // 其余代码保持不变 ); }
3. 完善fetchProfileImageUrl的返回逻辑
确保所有分支都有明确返回值:
Future<String?> fetchProfileImageUrl() async { String uid = auth.currentUser?.uid ?? ""; if (uid.isEmpty){ return null; } final profileImageRef = storage.ref().child("/users/$uid/$uid"); try { final String downloadUrl = await profileImageRef.getDownloadURL(); print("Got download URL"); return downloadUrl; } on FirebaseException catch (e) { print("Could not load image: $e"); return null; // 明确返回null } }
4. 指定FutureBuilder的泛型类型
替换FutureBuilder<dynamic>为FutureBuilder<String?>,获得类型安全:
return FutureBuilder<String?>( future: storage.fetchProfileImageUrl(), builder: (BuildContext context, AsyncSnapshot<String?> imageUrl) { // 后续代码无需强制转换,直接使用imageUrl.data }, );
内容的提问来源于stack exchange,提问作者Zorgan
相关产品推荐
相关产品推荐

