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

Flutter异步图片上传API中添加加载Spinner与结果弹窗需求

解决Flutter图片上传时加载Spinner及结果弹窗问题

问题修复点

  1. 修正异步状态管理:_isInAsyncCall需在请求开始前通过setState设为true,请求结束(无论成功失败)设为false
  2. 修复请求响应处理:移除错误的.then((value) => null),正确获取请求响应流
  3. 添加异常捕获:处理网络请求、JSON解析等可能的异常
  4. 实现加载Spinner遮罩:用Stack包裹页面内容,根据异步状态显示半透明遮罩和加载指示器
  5. 添加结果弹窗:请求完成后用showDialog展示API返回的消息

修改后的完整代码

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:http/http.dart' as http;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'find waste collector'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  File? selectedImage;
  String? message;
  bool _isInAsyncCall = false;

  Future<void> uploadImage() async {
    if (selectedImage == null) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('请先选择图片')),
        );
      }
      return;
    }

    try {
      // 开始请求,显示加载Spinner
      setState(() {
        _isInAsyncCall = true;
      });

      final request = http.MultipartRequest(
        "POST",
        Uri.parse('https://f77f-2402-d000-a500-73e5-8c48-4e9a-dad0-c14f.in.ngrok.io/upload'),
      );
      // MultipartRequest会自动设置Content-Type为multipart/form-data,无需手动添加
      request.files.add(http.MultipartFile(
        "image",
        selectedImage!.readAsBytes().asStream(),
        selectedImage!.lengthSync(),
        filename: selectedImage!.path.split("/").last,
      ));
      request.fields['userId'] = 'value';

      // 正确获取响应流
      final streamedResponse = await request.send();
      final res = await http.Response.fromStream(streamedResponse);
      final resJson = jsonDecode(res.body);
      message = resJson['message'];

      // 请求成功,显示结果弹窗
      if (mounted) {
        showDialog(
          context: context,
          builder: (ctx) => AlertDialog(
            title: const Text('上传结果'),
            content: Text(message ?? '未获取到响应消息'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(ctx),
                child: const Text('确定'),
              ),
            ],
          ),
        );
      }
    } catch (e) {
      // 捕获异常,提示错误
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('上传失败:${e.toString()}')),
        );
      }
    } finally {
      // 请求结束,隐藏加载Spinner
      if (mounted) {
        setState(() {
          _isInAsyncCall = false;
        });
      }
    }
  }

  Future<void> getImage() async {
    final pickedImage = await ImagePicker().pickImage(source: ImageSource.gallery);
    if (pickedImage != null) {
      setState(() {
        selectedImage = File(pickedImage.path);
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Stack(
        children: [
          Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                selectedImage == null
                    ? const Text("please pick a image to upload")
                    : Image.file(selectedImage!),
                const SizedBox(height: 20),
                TextButton.icon(
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all(
                      const Color.fromARGB(255, 62, 187, 0),
                    ),
                  ),
                  onPressed: uploadImage,
                  icon: const Icon(Icons.upload_file_rounded, color: Colors.black38),
                  label: const Text(
                    "Upload waste sample Image",
                    style: TextStyle(color: Colors.black),
                  ),
                ),
              ],
            ),
          ),
          // 加载遮罩和Spinner
          if (_isInAsyncCall)
            Container(
              color: Colors.black.withOpacity(0.5),
              child: const Center(
                child: CircularProgressIndicator(color: Colors.white),
              ),
            ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: getImage,
        child: const Icon(Icons.add_a_photo),
      ),
    );
  }
}

关键修改说明

  • 异步状态控制:在uploadImage开头通过setState切换加载状态,finally块确保请求结束后无论结果如何都隐藏加载状态
  • 响应处理修复:移除错误的响应截断代码,正确保留请求返回的流数据用于后续解析
  • 冗余Header移除:MultipartRequest会自动生成符合规范的请求头,手动添加会导致冲突
  • 加载遮罩实现:通过Stack层叠页面内容与遮罩,异步请求时显示半透明背景和加载指示器,阻止用户操作
  • 结果展示与异常处理:成功时弹出对话框展示API返回消息,异常时通过底部提示告知用户错误信息
  • 空安全处理:添加图片选择判断避免空指针,用mounted确保组件存活状态下执行状态更新与弹窗操作

内容的提问来源于stack exchange,提问作者00gash

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 12:31:05