[Flutter] httpパッケージのMultipartRequestでファイルをアップロードする

2024-10-06 hit count image

FlutterでhttpパッケージのMultipartRequestを使ってファイルをアップロードする方法と、それをテストする方法について説明します。

概要

Flutterでhttpパッケージを使ってファイルをアップロードする必要がある場合があります。この場合、httpパッケージのMultipartRequestを使うとファイルをアップロードできます。このブログポストでは、MultipartRequestを使ってファイルをアップロードする方法と、それをテストする方法について説明します。

MultipartRequestでファイルをアップロードする

次は実際にhttpパッケージのMultipartRequestを使ってファイルをアップロードする例です。

class ExampleAPI {
  final String token;
  final http.MultipartRequest httpClient;

  ExampleAPI({
    required this.token,
    @visibleForTesting http.MultipartRequest? mockClient,
  }) : httpClient = mockClient ??
            http.MultipartRequest(
              "POST",
              Uri.parse('${ENV.apiServer}/api/app'),
            );

  Future<InfoData> sendData({
    required DateTime date,
    required int status,
    required int docType,
    required String note,
    required String passportImage,
    String? certificationImage,
    List<String>? receiptImages,
  }) async {
    Map<String, String> data = {};
    data['date'] = dateFormatForSearch(date);
    data['status'] = '$status';
    data['doc_type'] = '$docType';
    data['note'] = note;

    List<MultipartFile> files = [];
    files.add(await http.MultipartFile.fromPath('passport_image', passportImage));
    if (jpnCertImg != null && jpnCertImg != '') {
      files.add(await http.MultipartFile.fromPath('cert_img', certificationImage));
    }
    if (receiptImages?.isNotEmpty == true) {
      for (var image in receiptImages!) {
        if (image == '') continue;
        files.add(await http.MultipartFile.fromPath('receipt_img[]', image));
      }
    }

    httpClient.headers.addAll({'Authorization': 'Bearer $token'});
    httpClient.fields.addAll(data);
    httpClient.files.addAll(files);

    final stream = await httpClient.send();
    return http.Response.fromStream(stream).then((response) {
      final data = jsonDecode(utf8.decode(response.bodyBytes));

      if (data['success'] == true) {
        return InfoData.fromJson(data['data']);
      } else {
        throw Exception('Unknown response');
      }
    });
  }
}

httpパッケージのMultipartRequestを使ってファイルをアップロードする部分をもう少し詳しく見ていきます。

ExampleAPI({
  required this.token,
  @visibleForTesting http.MultipartRequest? mockClient,
}) : httpClient = mockClient ??
          http.MultipartRequest(
            "POST",
            Uri.parse('${ENV.apiServer}/api/app'),
          );

MultiPartRequestでファイルをアップロードするには、まずhttp.MultipartRequestのインスタンスを作成する必要があります。

Map<String, String> data = {};
data['date'] = dateFormatForSearch(date);
data['status'] = '$status';
data['doc_type'] = '$docType';
data['note'] = note;

MultipartRequestはファイル以外にも他の情報を一緒に送信できます。そのため、一緒に送信する他の情報を準備しました。

List<MultipartFile> files = [];
files.add(await http.MultipartFile.fromPath('passport_image', passportImage));
if (jpnCertImg != null && jpnCertImg != '') {
  files.add(await http.MultipartFile.fromPath('cert_img', certificationImage));
}
if (receiptImages?.isNotEmpty == true) {
  for (var image in receiptImages!) {
    if (image == '') continue;
    files.add(await http.MultipartFile.fromPath('receipt_img[]', image));
  }
}

ファイルを設定する関数はアップロードしたいファイルのパスを受け取ります。受け取ったファイルのパスをhttp.MultipartFile.fromPathを使ってMultipartFileに変換します。

複数のファイルをアップロードする場合は、receipt_img[]のように[]を使って配列で渡すことができます。

httpClient.headers.addAll({'Authorization': 'Bearer $token'});
httpClient.fields.addAll(data);
httpClient.files.addAll(files);

次はファイルをアップロードするためにBearerトークンをヘッダーに設定し、準備したデータをfieldsに追加します。準備したファイルはfilesに追加します。

final stream = await httpClient.send();
return http.Response.fromStream(stream).then((response) {
  final data = jsonDecode(utf8.decode(response.bodyBytes));

  if (data['success'] == true) {
    return InfoData.fromJson(data['data']);
  } else {
    throw Exception('Unknown response');
  }
});

最後はデータを送信し、レスポンスを受け取ります。受け取ったレスポンスをアプリに合わせて処理すれば完了です。

テストコード

httpパッケージのMultipartRequestを使ってファイルをアップロードする関数をテストする方法について説明します。

まず、すべてのテストコードは次のようになります。

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  setUp(() {
    mockHttpMultipartRequest.headers.clear();
    mockHttpMultipartRequest.fields.clear();
    mockHttpMultipartRequest.files.clear();
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': true,
          'data': {
            'amount': 1000,
            'commission': 3000,
          }
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );
  });

  test('Success', () async {
    final result = await TaxRefundAPI(
      token: 'test_token',
      client: mockHttpMultipartRequest,
    ).sendData(
      permitDate: DateTime.parse('2022-01-01 01:04'),
      status: 91,
      docType: 2,
      note: '',
      passportImg: 'assets/images/passport_sample.jpg',
      certificationImage: 'assets/images/placeholder.png',
      receiptImages: [
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
      ],
    );

    // Request parameters
    expect(
      mockHttpMultipartRequest.headers,
      {'Authorization': 'Bearer test_token'},
    );
    expect(
      mockHttpMultipartRequest.fields,
      {
        'date': '2022-01-01',
        'status': '91',
        'doc_type': '2',
        'note': '',
      },
    );
    expect(mockHttpMultipartRequest.files.length, 5);
    expect(mockHttpMultipartRequest.files[0].field, 'passport_image');
    expect(mockHttpMultipartRequest.files[0].filename, 'passport_sample.jpg');
    expect(mockHttpMultipartRequest.files[1].field, 'cert_img');
    expect(mockHttpMultipartRequest.files[1].filename, 'placeholder.png');
    expect(mockHttpMultipartRequest.files[2].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[2].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[3].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[3].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[4].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[4].filename,
      'sample_receipt.png',
    );

    // Response
    expect(result, isA<InfoData>());
    expect(result.toMap(), {
      'amount': 1000,
      'commission': 3000,
    });
  });

  test('Throw error when response is failed', () async {
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': false,
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );

    try {
      await TaxRefundAPI(
        token: 'test_token',
        client: mockHttpMultipartRequest,
      ).sendData(
        permitDate: DateTime.parse('2022-01-01 01:04'),
        status: 91,
        docType: 2,
        note: '',
        passportImg: 'assets/images/passport_sample.jpg',
        certificationImage: 'assets/images/placeholder.png',
        receiptImages: [
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
        ],
      );
    } catch (e) {
      expect(e.toString(), 'Exception: Unknown response');
    }
  });
}

class CustomMockMultipartRequest extends MockMultipartRequest {
  @override
  final Map<String, String> headers = {};

  @override
  final Map<String, String> fields = {};

  @override
  final List<MultipartFile> files = [];
}

もっと詳しく見ていきましょう。

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  setUp(() {
    mockHttpMultipartRequest.headers.clear();
    mockHttpMultipartRequest.fields.clear();
    mockHttpMultipartRequest.files.clear();
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': true,
          'data': {
            'amount': 1000,
            'commission': 3000,
          }
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );
  });
  ...
}

class CustomMockMultipartRequest extends MockMultipartRequest {
  @override
  final Map<String, String> headers = {};

  @override
  final Map<String, String> fields = {};

  @override
  final List<MultipartFile> files = [];
}

DI(Dependency Injection)を使っているため、テスト用のMockオブジェクトを作成し、setUp関数を使ってMockオブジェクトを初期化しました。

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  ...
  test('Success', () async {
    final result = await TaxRefundAPI(
      token: 'test_token',
      client: mockHttpMultipartRequest,
    ).sendData(
      permitDate: DateTime.parse('2022-01-01 01:04'),
      status: 91,
      docType: 2,
      note: '',
      passportImg: 'assets/images/passport_sample.jpg',
      certificationImage: 'assets/images/placeholder.png',
      receiptImages: [
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
        'assets/images/sample_receipt.png',
      ],
    );

    // Request parameters
    expect(
      mockHttpMultipartRequest.headers,
      {'Authorization': 'Bearer test_token'},
    );
    expect(
      mockHttpMultipartRequest.fields,
      {
        'date': '2022-01-01',
        'status': '91',
        'doc_type': '2',
        'note': '',
      },
    );
    expect(mockHttpMultipartRequest.files.length, 5);
    expect(mockHttpMultipartRequest.files[0].field, 'passport_image');
    expect(mockHttpMultipartRequest.files[0].filename, 'passport_sample.jpg');
    expect(mockHttpMultipartRequest.files[1].field, 'cert_img');
    expect(mockHttpMultipartRequest.files[1].filename, 'placeholder.png');
    expect(mockHttpMultipartRequest.files[2].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[2].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[3].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[3].filename,
      'sample_receipt.png',
    );
    expect(mockHttpMultipartRequest.files[4].field, 'receipt_img[]');
    expect(
      mockHttpMultipartRequest.files[4].filename,
      'sample_receipt.png',
    );

    // Response
    expect(result, isA<InfoData>());
    expect(result.toMap(), {
      'amount': 1000,
      'commission': 3000,
    });
  });
  ...
}
...

setUp関数で事前に設定したMockオブジェクトを使って、リクエストが成功した場合をテストしました。

...
@GenerateMocks([http.MultipartRequest])
void main() {
  final mockHttpMultipartRequest = CustomMockMultipartRequest();
  ...
  test('Throw error when response is failed', () async {
    when(mockHttpMultipartRequest.send()).thenAnswer(
      (_) async {
        final responseBody = jsonEncode({
          'success': false,
        });
        final stream = Stream.value(utf8.encode(responseBody));
        return http.StreamedResponse(stream, 200);
      },
    );

    try {
      await TaxRefundAPI(
        token: 'test_token',
        client: mockHttpMultipartRequest,
      ).sendData(
        permitDate: DateTime.parse('2022-01-01 01:04'),
        status: 91,
        docType: 2,
        note: '',
        passportImg: 'assets/images/passport_sample.jpg',
        certificationImage: 'assets/images/placeholder.png',
        receiptImages: [
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
          'assets/images/sample_receipt.png',
        ],
      );
    } catch (e) {
      expect(e.toString(), 'Exception: Unknown response');
    }
  });
}
...

最後は、レスポンスが失敗した場合をテストしました。

完了

これで、FlutterでhttpパッケージのMultipartRequestを使ってファイルをアップロードする方法と、それをテストする方法について説明しました。

Flutterでファイルをアップロードする機能を実装する場合は、このブログを参考にして実装し、テストコードを書いてみてください。

私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!

アプリ広報

今見てるブログを作成たDekuが開発したアプリを使ってみてください。
Dekuが開発したアプリはFlutterで開発されています。

興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。

Posts