Flutter中Image.memory数据源变更后组件不刷新问题求助
问题描述
我有一个允许用户上传图片格式文档的页面,为减少重复代码,每个可上传的文档类型都复用Document_Upload组件。
初始加载时我使用FutureBuilder从REST Api获取用户已上传的所有文档,再将对应数据填充到每个Document_Upload组件中。
上传成功后,我们的REST Api会将新图片以字节数组的形式返回给Flutter应用用于展示。
我目前遇到的问题是,无论尝试什么方案,Image.memory组件都不展示新图片,一直停留在旧图。我已经尝试了几乎所有能想到或网上查到的解决方案,包括:
- 更新imageString变量后调用
setState({}),能看到组件闪烁但仍然显示原始图片。 - 通过函数回调通知父组件重绘整个子组件树,结果和setState一致,所有组件闪烁但无更新。
- 更新imageString前调用
imageCache.clear()与imageCache.clearLiveImages()。 - 用
CircleAvatar替代Image.memory。 - 在setState中重新调用
new Image.memory()重建Image组件。
我开始怀疑这是Image.memory本身的问题,但根据当前需求,无法使用Image.File/Image.network方案。手动刷新页面后新图片可以正常显示。
代码实现
documents_page.dart
class DocumentsPage extends StatefulWidget { @override _DocumentsPageState createState() => _DocumentsPageState(); } class _DocumentsPageState extends State<DocumentsPage> with SingleTickerProviderStateMixin { Future<Personal> _getUserDocuments; Personal _documents; @override void didChangeDependencies() { super.didChangeDependencies(); _getUserDocuments = sl<AccountProvider>().getUserDocuments(); } @override Widget build(BuildContext context) { return SingleChildScrollView( child: SafeArea( child: Center( child: Padding( padding: EdgeInsets.all(20), child: Container( constraints: BoxConstraints(maxWidth: 1300), child: buildFutureBuilder(context)), )), ), ); } Widget buildFutureBuilder(BuildContext context) { var screenSize = MediaQuery.of(context).size; return FutureBuilder<Personal>( future: _getUserDocuments, builder: (context, AsyncSnapshot<Personal> snapshot) { if (!snapshot.hasData) { return Text("Loading"); } else { if (snapshot.data == null) { return Center(child: Text('Error: ${snapshot.error}')); } else { _documents = snapshot.data; return Column( children: [ SizedBox(height: 20.0), Text( "DOCUMENTS", textAlign: TextAlign.center, style: TextStyle( fontSize: 25, fontWeight: FontWeight.bold, color: AppColors.navy), ), Container( constraints: BoxConstraints(maxWidth: 250), child: Divider( color: AppColors.darkBlue, height: 20, ), ), Container( margin: EdgeInsets.only(top: 5.0, bottom: 5.0), child: Text( "These documents are required in order to verify you as a user", style: TextStyle(fontSize: 14))), Container( margin: EdgeInsets.only(bottom: 25.0), child: Text("View our Privacy Policy", style: TextStyle(fontSize: 14))), Container( child: screenSize.width < 768 ? Column( children: [ DocumentUpload( imageType: "ID", imageString: _documents.id), DocumentUpload( imageType: "Drivers License Front", imageString: _documents.driversLicenseFront, ), DocumentUpload( imageType: "Drivers License Back", imageString: _documents.driversLicenseBack, ) ], ) : Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ DocumentUpload( imageType: "ID", imageString: _documents.id), DocumentUpload( imageType: "Drivers License Front", imageString: _documents.driversLicenseFront, ), DocumentUpload( imageType: "Drivers License Back", imageString: _documents.driversLicenseBack, ), ])), Container( child: screenSize.width < 768 ? Container() : Padding( padding: EdgeInsets.only(top: 10.0, bottom: 10.0))), Container( child: screenSize.width < 768 ? Column( children: [ DocumentUpload( imageType: "Selfie", imageString: _documents.selfie, ), DocumentUpload( imageType: "Proof of Residence", imageString: _documents.proofOfResidence, ), Container(width: 325) ], ) : Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ DocumentUpload( imageType: "Selfie", imageString: _documents.selfie, ), DocumentUpload( imageType: "Proof of Residence", imageString: _documents.proofOfResidence, ), Container(width: 325) ])), ], ); } } }); } }
document_upload.dart
class DocumentUpload extends StatefulWidget { final String imageType; final String imageString; const DocumentUpload({this.imageType, this.imageString}); @override _DocumentUploadState createState() => _DocumentUploadState(); } class _DocumentUploadState extends State<DocumentUpload> { String _imageType; String _imageString; bool uploadPressed = false; Image _imageWidget; @override Widget build(BuildContext context) { setState(() { _imageType = widget.imageType; _imageString = widget.imageString; _imageWidget = new Image.memory(base64Decode(_imageString), fit: BoxFit.fill); }); return Container( constraints: BoxConstraints(maxWidth: 325), height: 200, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), boxShadow: [ new BoxShadow( color: AppColors.lightGrey, blurRadius: 5.0, offset: Offset(0.0, 3.0), ), ], ), child: Card( color: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20.0), ), child: Column(children: <Widget>[ Padding(padding: EdgeInsets.only(top: 5.0)), Row( //ROW 1 children: <Widget>[ Expanded( child: Text( _imageType, textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.darkBlue), ), ), ], ), Row( //ROW 2 children: <Widget>[ Expanded( child: Container( padding: EdgeInsets.only(left: 5.0, bottom: 5.0), child: ClipRRect( borderRadius: BorderRadius.circular(20.0), child: _imageWidget, )), ), Consumer<AccountProvider>( builder: (context, provider, child) { return Padding( padding: EdgeInsets.all(10.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Padding( padding: EdgeInsets.only(top: 5.0, bottom: 5.0), child: Icon(Icons.star, size: 20, color: AppColors.darkBlue)), Padding( padding: EdgeInsets.only(top: 5.0, bottom: 5.0), child: Text('Drag file here or', textAlign: TextAlign.center)), Padding( padding: EdgeInsets.only(top: 5.0, bottom: 5.0), child: DynamicGreyButton( title: uploadPressed ? "Uploading ..." : "Browse", onPressed: () async { FilePickerResult result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: [ 'jpg', 'jpeg', 'png' ]); if (result != null) { uploadPressed = true; Uint8List file = result.files.single.bytes; String fileType = result.files.single.extension; await provider .doUploadDocument( _imageType, file, fileType) .then((uploadResult) { if (uploadResult == null || uploadResult == '') { showToast( "Document failed to upload"); return; } else { showToast("Document uploaded", Colors.green, "#66BB6A"); uploadPressed = false; _imageString = uploadResult; setState(() {}); } }); } else { // User canceled the picker uploadPressed = false; } }, )) ])); }) ], ), ]))); } }
图片上传HTTP请求代码
@override Future uploadDocuments(DocumentsUpload model) async { final response = await client.post( Uri.https(appConfig.baseUrl, "/api/Account/PostDocuments_Flutter"), body: jsonEncode(model.toJson()), headers: <String, String>{ 'Content-Type': 'application/json' }); if (response.statusCode == 200) { var data = json.decode(response.body); return data; } else { return ""; } }
运行效果

解决方案
核心问题定位
- 子组件
DocumentUpload上传成功后仅更新了自身内部的_imageString变量,父组件DocumentsPage中存储的_documents对象对应字段没有同步更新,子组件下次重绘时build方法里的setState会把_imageString重新覆盖为父组件传入的旧值。 Image组件的缓存匹配规则导致即使字节更新也可能复用旧渲染结果。
修复步骤
步骤1:新增上传成功回调,同步更新父组件数据
修改DocumentUpload组件,添加上传成功回调参数:
class DocumentUpload extends StatefulWidget { final String imageType; final String imageString; // 新增回调,返回新的base64字符串 final Function(String newImageBase64) onUploadSuccess; const DocumentUpload({ required this.imageType, required this.imageString, required this.onUploadSuccess, }); }
上传成功后调用回调通知父组件:
// 上传成功逻辑处修改 if (uploadResult == null || uploadResult == '') { showToast("Document failed to upload"); return; } else { showToast("Document uploaded", Colors.green, "#66BB6A"); uploadPressed = false; // 调用回调通知父组件更新数据 widget.onUploadSuccess(uploadResult); setState(() {}); }
步骤2:父组件监听回调,更新_documents对应字段
父组件使用DocumentUpload时传入回调,根据imageType更新对应字段:
// 以ID类型为例,其他组件同理修改 DocumentUpload( imageType: "ID", imageString: _documents.id, onUploadSuccess: (newBase64) { setState(() { _documents.id = newBase64; }); }, )
步骤3:给Image.memory添加唯一key,强制重绘
修改Image.memory的构造参数,添加gaplessPlayback: false和唯一key,避免缓存复用:
_imageWidget = Image.memory( base64Decode(_imageString), fit: BoxFit.fill, gaplessPlayback: false, // 用base64的哈希值作为key,内容变化时自动重建组件 key: ValueKey(_imageString.hashCode), );
步骤4:移除build方法中的无效setState
将初始化逻辑移到didUpdateWidget生命周期中,避免不必要的重绘:
@override void didUpdateWidget(covariant DocumentUpload oldWidget) { super.didUpdateWidget(oldWidget); if (widget.imageString != oldWidget.imageString) { _imageString = widget.imageString; _imageWidget = Image.memory( base64Decode(_imageString), fit: BoxFit.fill, gaplessPlayback: false, key: ValueKey(_imageString.hashCode), ); } if (widget.imageType != oldWidget.imageType) { _imageType = widget.imageType; } }
内容的提问来源于stack exchange,提问作者unselected
相关产品推荐
相关产品推荐

