MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

在 Flutter 中使用 http 插件进行文件上传与下载

2021-02-282.5k 阅读

一、Flutter 中 HTTP 基础知识

在 Flutter 应用开发中,与服务器进行数据交互是非常常见的需求。而 HTTP(Hypertext Transfer Protocol)协议是在网络应用中用于传输数据的基础协议。理解 HTTP 协议的基本概念对于使用 http 插件进行文件上传与下载至关重要。

(一)HTTP 请求方法

  1. GET
    • GET 方法主要用于从服务器获取数据。它将请求参数附加在 URL 后面,以键值对的形式呈现。例如,请求获取用户信息,URL 可能是 https://example.com/api/user?id=123,其中 id=123 就是请求参数。
    • GET 方法的优点是简单、快速,并且请求参数在 URL 中可见,方便调试。但缺点是 URL 长度有限制,并且不适合传输敏感数据,因为参数直接暴露在 URL 中。
  2. POST
    • POST 方法用于向服务器提交数据。与 GET 不同,POST 请求的数据是放在请求体(body)中的,而不是 URL 中。这使得它可以传输大量的数据,并且相对更安全,因为敏感数据不会在 URL 中暴露。
    • 在文件上传场景中,POST 方法是常用的请求方法,因为文件数据量通常较大,不适合放在 URL 中。
  3. PUT
    • PUT 方法一般用于更新服务器上的资源。它会将整个资源的最新状态发送到服务器,服务器会根据请求数据更新对应的资源。例如,更新用户的详细信息,可能会使用 PUT 请求将新的用户信息发送到服务器。
  4. DELETE
    • DELETE 方法用于从服务器删除指定的资源。例如,删除用户的某个文件,就可以使用 DELETE 请求,请求 URL 中指定要删除文件的路径。

(二)HTTP 响应状态码

  1. 1xx(信息性状态码)
    • 这类状态码表示服务器已经收到请求,正在处理中。例如,100 Continue 表示客户端可以继续发送请求的剩余部分。在实际的 Flutter 应用与服务器交互中,1xx 状态码相对较少直接处理,因为 Flutter 应用通常更关注最终的处理结果。
  2. 2xx(成功状态码)
    • 200 OK 是最常见的成功状态码,表示请求已成功处理。当进行文件下载时,如果服务器返回 200 OK,说明文件下载请求成功,接下来可以处理返回的文件数据。201 Created 则表示请求成功,并且服务器创建了新的资源,在文件上传成功创建新文件时可能会返回此状态码。
  3. 3xx(重定向状态码)
    • 301 Moved Permanently 表示请求的资源已被永久移动到新的 URL,302 Found 表示资源临时移动到新的 URL。在 Flutter 应用中,如果遇到这类状态码,通常需要根据新的 URL 重新发起请求。
  4. 4xx(客户端错误状态码)
    • 400 Bad Request 表示客户端发送的请求有语法错误,服务器无法理解。在文件上传时,如果请求格式不正确,可能会收到此状态码。401 Unauthorized 表示请求需要用户认证,而客户端未提供有效的认证信息,这在需要登录才能进行文件操作的场景中较为常见。404 Not Found 表示服务器找不到请求的资源,比如文件下载时指定的文件不存在。
  5. 5xx(服务器错误状态码)
    • 500 Internal Server Error 表示服务器内部发生错误,无法完成请求。这可能是服务器端代码出现了异常,在文件上传或下载过程中,如果服务器处理逻辑有问题,可能会返回此状态码。503 Service Unavailable 表示服务器暂时无法处理请求,通常是因为服务器过载或正在维护。

二、引入 http 插件

在 Flutter 项目中使用 http 插件进行文件上传与下载,首先需要在 pubspec.yaml 文件中添加 http 依赖。

(一)添加依赖

打开 pubspec.yaml 文件,在 dependencies 部分添加以下内容:

http: ^0.13.5

这里的 ^0.13.5 表示使用 http 插件的版本号,^ 符号表示会使用大于等于 0.13.5 且小于 0.14.0 的版本。这样可以在保证兼容性的前提下,获取一些小版本的更新和修复。

(二)安装依赖

添加完依赖后,在项目根目录下打开终端,运行以下命令安装依赖:

flutter pub get

此命令会从 Pub 仓库下载 http 插件及其依赖,并将其添加到项目中。安装完成后,就可以在 Flutter 代码中导入并使用 http 插件了。

(三)导入 http 库

在需要使用 http 功能的 Dart 文件中,导入 http 库:

import 'package:http/http.dart' as http;

这里使用 as http 是为了给导入的库指定一个别名,这样在代码中使用 http 相关功能时,就可以通过这个别名来调用,避免与其他可能重名的库或变量冲突。

三、文件下载

在 Flutter 应用中实现文件下载,需要向服务器发送 GET 请求,并处理服务器返回的文件数据。

(一)简单文件下载示例

以下是一个简单的文件下载示例,假设服务器上有一个图片文件,我们要将其下载并保存到本地:

import 'dart:io';
import 'package:http/http.dart' as http;

Future<void> downloadFile(String url, String savePath) async {
  final response = await http.get(Uri.parse(url));
  if (response.statusCode == 200) {
    final file = File(savePath);
    await file.writeAsBytes(response.bodyBytes);
    print('文件下载成功,保存路径:$savePath');
  } else {
    print('文件下载失败,状态码:${response.statusCode}');
  }
}

在这个示例中:

  1. 首先定义了 downloadFile 函数,它接受两个参数:url 表示要下载文件的服务器地址,savePath 表示文件在本地保存的路径。
  2. 使用 http.get 方法发送 GET 请求到指定的 url,并等待服务器响应。Uri.parse(url) 用于将字符串形式的 URL 转换为 Uri 对象,这是 http 插件发送请求所需要的格式。
  3. 检查响应的状态码,如果状态码为 200,表示下载成功。此时,通过 response.bodyBytes 获取服务器返回的文件字节数据,并使用 File(savePath).writeAsBytes 将这些字节数据写入到本地指定路径的文件中。
  4. 如果状态码不是 200,则打印下载失败的信息,包括状态码。

(二)处理大文件下载

对于大文件下载,直接将所有数据一次性写入本地文件可能会导致内存问题。可以采用分块下载的方式,逐步将数据写入文件。以下是一个改进的大文件下载示例:

import 'dart:io';
import 'package:http/http.dart' as http;

Future<void> downloadLargeFile(String url, String savePath) async {
  final request = await http.get(Uri.parse(url), headers: {'Accept-Encoding': 'identity'});
  final file = File(savePath);
  final sink = file.openWrite();
  final response = http.ByteStream(request.bodyStream);
  await response.pipe(sink);
  await sink.close();
  if (request.statusCode == 200) {
    print('大文件下载成功,保存路径:$savePath');
  } else {
    print('大文件下载失败,状态码:${request.statusCode}');
  }
}

在这个示例中:

  1. 使用 http.get 发送请求,并在 headers 中设置 Accept - Encoding: identity。这是为了告诉服务器不要对响应数据进行压缩,因为分块处理时我们希望直接处理原始数据。
  2. 创建一个本地文件对象 File(savePath),并通过 file.openWrite() 打开一个写入流 sink
  3. 使用 http.ByteStream(request.bodyStream) 获取服务器返回数据的字节流 response
  4. 通过 response.pipe(sink) 将字节流逐步写入到本地文件的写入流中,实现分块下载。
  5. 最后关闭写入流 sink.close(),并根据响应状态码判断下载是否成功。

(三)显示下载进度

为了提升用户体验,在文件下载过程中显示下载进度是很有必要的。可以通过监听 http.StreamedResponsecontentLength 和已接收的字节数来计算下载进度。以下是一个带下载进度显示的文件下载示例:

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

class DownloadPage extends StatefulWidget {
  @override
  _DownloadPageState createState() => _DownloadPageState();
}

class _DownloadPageState extends State<DownloadPage> {
  double _progress = 0.0;

  Future<void> downloadFileWithProgress(String url, String savePath) async {
    final response = await http.get(Uri.parse(url), headers: {'Accept-Encoding': 'identity'});
    final file = File(savePath);
    final totalLength = response.contentLength;
    var downloaded = 0;
    final sink = file.openWrite();
    final stream = response.stream;
    stream.listen((data) {
      downloaded += data.length;
      setState(() {
        _progress = totalLength == null? 0 : downloaded / totalLength;
      });
      sink.add(data);
    }, onDone: () {
      sink.close();
      if (response.statusCode == 200) {
        print('文件下载成功,保存路径:$savePath');
      } else {
        print('文件下载失败,状态码:${response.statusCode}');
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('文件下载'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
            onPressed: () {
              downloadFileWithProgress('https://example.com/file.jpg', '/sdcard/download/file.jpg');
            },
            child: Text('开始下载'),
          ),
          if (_progress > 0)
            LinearProgressIndicator(
              value: _progress,
            )
        ],
      ),
    );
  }
}

在这个示例中:

  1. 创建了一个 DownloadPage 页面,包含一个下载按钮和一个用于显示下载进度的 LinearProgressIndicator
  2. _DownloadPageState 中定义了 _progress 变量来保存当前下载进度。
  3. downloadFileWithProgress 函数中,通过 response.contentLength 获取文件总长度 totalLength,并在 stream.listen 中监听数据接收情况。每次接收到数据时,更新已下载的字节数 downloaded,并通过 setState 更新 _progress 的值,从而更新进度条的显示。
  4. 当数据接收完成(onDone)时,关闭写入流,并根据状态码判断下载是否成功。

四、文件上传

在 Flutter 中使用 http 插件进行文件上传,通常使用 POST 请求,并将文件数据放在请求体中。

(一)单文件上传示例

以下是一个简单的单文件上传示例,假设要将本地的一张图片上传到服务器:

import 'dart:io';
import 'package:http/http.dart' as http;

Future<void> uploadFile(String url, String filePath) async {
  final file = File(filePath);
  final request = http.MultipartRequest('POST', Uri.parse(url));
  final multipartFile = await http.MultipartFile.fromPath('file', filePath);
  request.files.add(multipartFile);
  final response = await request.send();
  if (response.statusCode == 200) {
    print('文件上传成功');
  } else {
    print('文件上传失败,状态码:${response.statusCode}');
  }
}

在这个示例中:

  1. 定义了 uploadFile 函数,接受 url(服务器接收文件的地址)和 filePath(本地文件路径)作为参数。
  2. 创建一个 http.MultipartRequest 对象,指定请求方法为 POST 和请求地址 Uri.parse(url)
  3. 使用 http.MultipartFile.fromPath 从本地文件路径创建一个 MultipartFile 对象,这里的 'file' 是在服务器端接收文件时使用的参数名。
  4. MultipartFile 添加到 request.files 中。
  5. 发送请求 await request.send(),并根据响应状态码判断文件上传是否成功。

(二)多文件上传示例

有时候需要一次性上传多个文件,以下是一个多文件上传的示例:

import 'dart:io';
import 'package:http/http.dart' as http;

Future<void> uploadMultipleFiles(String url, List<String> filePaths) async {
  final request = http.MultipartRequest('POST', Uri.parse(url));
  for (var filePath in filePaths) {
    final file = File(filePath);
    final multipartFile = await http.MultipartFile.fromPath('files[]', filePath);
    request.files.add(multipartFile);
  }
  final response = await request.send();
  if (response.statusCode == 200) {
    print('多个文件上传成功');
  } else {
    print('多个文件上传失败,状态码:${response.statusCode}');
  }
}

在这个示例中:

  1. uploadMultipleFiles 函数接受 url 和一个包含多个本地文件路径的 List<String> 作为参数。
  2. 同样创建一个 http.MultipartRequest 对象。
  3. 通过循环遍历 filePaths,为每个文件创建一个 MultipartFile 对象,并添加到 request.files 中。这里的 'files[]' 是服务器端接收多个文件时使用的参数名约定,不同服务器端框架可能会有不同的约定,需要根据实际情况调整。
  4. 发送请求并根据响应状态码判断上传是否成功。

(三)上传进度显示

和文件下载类似,文件上传过程中显示上传进度可以提升用户体验。以下是一个带上传进度显示的文件上传示例:

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

class UploadPage extends StatefulWidget {
  @override
  _UploadPageState createState() => _UploadPageState();
}

class _UploadPageState extends State<UploadPage> {
  double _progress = 0.0;

  Future<void> uploadFileWithProgress(String url, String filePath) async {
    final file = File(filePath);
    final request = http.MultipartRequest('POST', Uri.parse(url));
    final multipartFile = await http.MultipartFile.fromPath('file', filePath);
    request.files.add(multipartFile);
    request.send().then((response) {
      if (response.statusCode == 200) {
        print('文件上传成功');
      } else {
        print('文件上传失败,状态码:${response.statusCode}');
      }
    });
    response.stream.listen((data) {
      setState(() {
        _progress = data.length / file.lengthSync();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('文件上传'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          ElevatedButton(
            onPressed: () {
              uploadFileWithProgress('https://example.com/upload', '/sdcard/image.jpg');
            },
            child: Text('开始上传'),
          ),
          if (_progress > 0)
            LinearProgressIndicator(
              value: _progress,
            )
        ],
      ),
    );
  }
}

在这个示例中:

  1. 创建了一个 UploadPage 页面,包含一个上传按钮和一个用于显示上传进度的 LinearProgressIndicator
  2. _UploadPageState 中定义了 _progress 变量来保存当前上传进度。
  3. uploadFileWithProgress 函数中,在发送请求 request.send() 后,通过监听 response.stream 来获取已上传的数据量。每次接收到数据时,通过 setState 更新 _progress 的值,data.length 表示已上传的数据长度,file.lengthSync() 表示文件总长度,从而实现上传进度的计算和显示。
  4. 根据响应状态码判断文件上传是否成功。

五、常见问题及解决方法

在使用 http 插件进行文件上传与下载过程中,可能会遇到一些常见问题。

(一)网络问题

  1. 网络连接失败
    • 原因:可能是设备当前没有网络连接,或者网络不稳定。
    • 解决方法:在发起请求前,可以使用 connectivity 插件检查网络连接状态。例如:
    import 'package:connectivity_plus/connectivity_plus.dart';
    
    Future<bool> checkNetwork() async {
      final connectivityResult = await (Connectivity().checkConnectivity());
      return connectivityResult != ConnectivityResult.none;
    }
    
    然后在进行文件上传或下载前,先调用 checkNetwork 方法,如果返回 false,提示用户检查网络连接。
  2. 请求超时
    • 原因:服务器响应时间过长,或者网络延迟较大。
    • 解决方法:可以在 http 请求中设置超时时间。例如:
    final response = await http.get(Uri.parse(url), headers: {'Accept-Encoding': 'identity'}, timeout: Duration(seconds: 10));
    
    这里设置了超时时间为 10 秒,如果 10 秒内没有收到服务器响应,会抛出 TimeoutException,可以在 catch 块中处理超时情况,提示用户请求超时,建议重试。

(二)服务器响应问题

  1. 状态码异常
    • 原因:服务器返回的状态码不是预期的成功状态码,如 404、500 等。
    • 解决方法:根据不同的状态码进行不同的处理。例如,如果返回 404 Not Found,提示用户请求的资源不存在;如果返回 500 Internal Server Error,提示用户服务器内部错误,建议稍后重试。可以在响应处理部分添加如下代码:
    if (response.statusCode == 404) {
      print('请求的文件不存在');
    } else if (response.statusCode == 500) {
      print('服务器内部错误,请稍后重试');
    }
    
  2. 响应数据格式不正确
    • 原因:服务器返回的数据格式与预期不符,例如在文件下载时,返回的数据不是文件的正确格式。
    • 解决方法:在处理响应数据前,先检查响应的 content - type 头信息,确保数据格式正确。例如:
    if (response.headers['content - type'] == 'image/jpeg') {
      // 处理 JPEG 图片数据
    } else {
      print('响应数据格式不正确');
    }
    

(三)文件处理问题

  1. 文件写入失败
    • 原因:可能是本地文件路径不存在、没有写入权限,或者磁盘空间不足。
    • 解决方法:在写入文件前,先检查文件路径是否存在,如果不存在则创建。例如:
    final file = File(savePath);
    final directory = file.parent;
    if (!await directory.exists()) {
      await directory.create(recursive: true);
    }
    
    同时,捕获写入文件时可能抛出的异常,如 FileSystemException,处理没有写入权限或磁盘空间不足等问题,提示用户相应的错误信息。
  2. 文件读取失败
    • 原因:本地文件不存在,或者没有读取权限。
    • 解决方法:在读取文件前,先检查文件是否存在,并且捕获读取文件时可能抛出的异常,如 FileSystemException。例如:
    final file = File(filePath);
    if (!await file.exists()) {
      print('文件不存在');
    } else {
      try {
        // 进行文件读取操作
      } catch (e) {
        if (e is FileSystemException) {
          print('文件读取失败,可能没有读取权限');
        }
      }
    }
    

通过以上对 Flutter 中使用 http 插件进行文件上传与下载的详细介绍,包括 HTTP 基础知识、插件引入、文件下载和上传的具体实现以及常见问题解决方法,开发者可以在 Flutter 应用中顺利实现高效、稳定的文件传输功能。