📠

Flutter (Dart) の API Client

2023/05/28に公開

概要

DartでHTTP通信を担うAPI Client作ります。(自明)
Dartコアライブラリである http のみの利用。
可用性とテスタビリティを良好にしつつ、シンプルな構成を目指します。

実装

実際に書いたコード
https://github.com/yokoyamark/api_client_example
今回は叩くAPIのモックとして https://httpbin.org/ を利用させていただきました。便利なのでおすすめ。

httpライブラリのリファレンスみるとお分かりになると思いますが、getpostが実行メソッドとして用意されています。ただ、利用すると手続的なコードを強いられるので使いません。リクエスト構造体(クラス)に情報集めてクライアントで実行するだけが好ましいです。
https://pub.dev/documentation/http/latest/http/http-library.html

HTTP通信の実体部分(ライブラリになげる処理)

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

class ApiCore {
  final http.Client _client = http.Client();
  final Duration _timeLimit = const Duration(seconds: 30);

  Future<http.Response> send(http.BaseRequest req) async {
    return http.Response.fromStream(
        await _client.send(req).timeout(_timeLimit));
  }
}

自パッケージ側でリクエストを組みたてやレスポンスの共通処理

class ApiClient {
  // 実務での利用では適切に依存関係を注入してください
  final api = ApiCore();

  Future<http.Response> execute(Request req) async {
    final http.Request request = await _build(req);
    try {
      final response = await api.send(request);
      if (200 <= response.statusCode && response.statusCode <= 299) {
        return response;
      } else {
        throw _createException(response);
      }
    } catch (_) {
      rethrow;
    }
  }

  Future<http.BaseRequest> _build(Request req) async {
    final Map<String, String> headers = await req.headers();
    final Map<String, String> query = await req.query();
    final Map<String, dynamic> body = await req.body();

    Uri url = Uri.https(req.domain, req.path, query);

    return http.Request(req.method.name.toUpperCase(), url)
      ..headers.addAll(headers)
      ..body = json.encode(body);
  }

  Exception _createException(http.Response res) {
    // 実務での利用では適切にアプリ定義Exceptionを発生させる
    return Exception({res.body});
  }
}

自パッケージ内で定義しているRequestクラスの例

abstract class Request {
  String domain = Constant.domain;

  abstract String path;

  abstract HTTPMethod method;

  Future<Map<String, String>> headers() async {
    return {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    };
  }

  Future<Map<String, String>> query() async => {};

  Future<Map<String, dynamic>> body() async => {};
}

実際にリクエストクラスを実装する例

class SampleGet extends Request {
  
  HTTPMethod method = HTTPMethod.get;

  
  String path = '/get';

  
  Future<Map<String, String>> query() async {
    return {
      'test': 'query',
    };
  }
}

最後に

今回のサンプルコードだとApiClientにApiCoreをがっつり入れ込んでいますが、ApiClient生成時に投げ込むなりProvider使うなりで、ApiCoreへの依存を切り分けてあげるとよいです。APIのモック化が捗ると思います。

返却されたResponseをどう処理するかはお好みです。
asyncライブラリにResult型があるので、利用することで、Swift的な非同期ハンドリングっぽくすることもできます。ただし、クロージャで処理が階層的になっていく点は一長一短もあります。
Dartの try-catch は非常に優秀で、awaitしている非同期処理のハンドリングもできます。
メリット・デメリットを考えてから、プロジェクトに合わせた形で処理しましょう。

ここらへんのとても参考になる記事
https://ntaoo.hatenablog.com/entry/2018/12/12/100132

Discussion