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

如何实现带附件上传的Flutter表单并发送Multipart POST请求?

问题排查与实现方案

1. readAsBytes报错原因

该报错有两个核心诱因:

  • 你的file变量未完成赋值,为空状态下调用实例方法触发异常
  • file变量类型不是dart:io库的File类型,比如你使用跨端文件选择工具拿到的是PlatformFile等封装类型,该类本身没有readAsBytes方法

另外你现有请求代码里手动设置Content-Type: text/plain是错误的,MultipartRequest会自动生成带边界标识的多部分表单请求头,手动覆盖会导致后端无法正确解析请求数据,需要删除这段header设置逻辑。

2. 完整实现步骤

你的Multipart请求思路本身没有问题,只需要补全文件选择、表单校验逻辑即可,具体实现如下:

第一步:添加依赖

在pubspec.yaml中添加文件选择和网络请求依赖:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  file_picker: ^6.1.1

执行flutter pub get完成依赖安装。

第二步:构建表单页面

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

class HomeworkFormPage extends StatefulWidget {
  const HomeworkFormPage({super.key});

  @override
  State<HomeworkFormPage> createState() => _HomeworkFormPageState();
}

class _HomeworkFormPageState extends State<HomeworkFormPage> {
  final _formKey = GlobalKey<FormState>();
  final TextEditingController _titleController = TextEditingController();
  final TextEditingController _limitDateController = TextEditingController();
  PlatformFile? _selectedFile;
  // 这两个参数根据你的实际业务逻辑赋值,这里写示例固定值
  final int _subjectId = 1;
  final int _teacherId = 10;

  // 选择TXT文件方法
  Future<void> _pickFile() async {
    FilePickerResult? result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['txt'], // 仅允许TXT格式
      allowMultiple: false, // 仅允许选1个文件
    );
    if (result != null) {
      setState(() {
        _selectedFile = result.files.first;
      });
    }
  }

  // 修正后的提交请求方法
  Future<void> _submitForm() async {
    if (_formKey.currentState!.validate() && _selectedFile != null) {
      var url = Uri.parse('http://localhost:8000/newHomework');
      var request = http.MultipartRequest('POST', url);
      
      // 添加文件
      request.files.add(http.MultipartFile.fromBytes(
        'text', // 确保和后端接收文件的字段名一致
        _selectedFile!.bytes!,
        filename: _selectedFile!.name, // 用选中的实际文件名
      ));
      
      // 添加表单字段
      request.fields['title'] = _titleController.text.trim();
      request.fields['limitDate'] = _limitDateController.text.trim();
      request.fields['subjectid'] = _subjectId.toString();
      request.fields['teacherid'] = _teacherId.toString();
      request.fields['ext'] = '.txt';

      var res = await request.send();
      final respStr = await res.stream.bytesToString();
      final parsedJson = json.decode(respStr);
      print(parsedJson);
      // 这里可以加请求成功/失败的交互提示
    } else if (_selectedFile == null) {
      // 提示未选择附件
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请选择TXT格式的附件')));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('提交作业')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: const InputDecoration(labelText: '作业标题'),
                validator: (value) => value?.trim().isEmpty == true ? '请输入标题' : null,
              ),
              TextFormField(
                controller: _limitDateController,
                decoration: const InputDecoration(labelText: '截止日期'),
                validator: (value) => value?.trim().isEmpty == true ? '请输入截止日期' : null,
              ),
              const SizedBox(height: 20),
              Row(
                children: [
                  ElevatedButton(
                    onPressed: _pickFile,
                    child: const Text('选择TXT附件'),
                  ),
                  const SizedBox(width: 10),
                  Expanded(child: Text(_selectedFile?.name ?? '未选择文件')),
                ],
              ),
              const SizedBox(height: 30),
              ElevatedButton(
                onPressed: _submitForm,
                child: const Text('提交数据'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

注意事项

  • 确保请求中文件对应的字段名text和后端接收文件的参数名一致,否则后端无法拿到上传的文件
  • 本地测试时如果用安卓模拟器,需要将localhost替换为模拟器对应的宿主机IP10.0.2.2,否则无法访问本地后端服务

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 18:27:07