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

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 "";
    }
  }

运行效果

运行效果演示


解决方案

核心问题定位

  1. 子组件DocumentUpload上传成功后仅更新了自身内部的_imageString变量,父组件DocumentsPage中存储的_documents对象对应字段没有同步更新,子组件下次重绘时build方法里的setState会把_imageString重新覆盖为父组件传入的旧值。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 11:27:00