YOUTRUSTアプリを支えるAPIリクエストの仕組み

みなさま、こんにちはashdikこと朝日(YOUTRUST / Twitter)です。

最近スマブラSP熱が再燃してしまい、ほぼ毎日のようにYouTubeを見ては修行を積んでいる毎日です。
レベルとしては、3体ほどVIPに行っています。

そして、実は社内にもスマブラ部があります笑
たまにオンラインで対戦したり、最近は会社対抗戦とかやりたいねーなんて話しています。
もし僕個人でもYOUTRUST社でも対戦したいと言う方がいらっしゃいましたらご連絡お待ちしております!

⚓️ 概要

YOUTRUSTアプリでは、アプリケーションサーバに向けてAPIリクエストを送っています。
APIリクエストの種類は全部で100種類〜ほどあります。
が、開放閉鎖原則に基づいていることもあり全く複雑になっていません。

そこで、そんな多くのAPIリクエストを抱えるYOUTRUSTアプリが
どのような形式でAPIリクエストを行なっているのかを本記事では解説したいと思います。

tech.youtrust.co.jp

↑は前回、僕が書いた記事です。 この記事に登場する図のうち、青枠部分の説明になります。

💻 筆者環境

Flutter 3.3.4
Dart 2.18.2
flutter_hooks 0.18.5+1
hooks_riverpod 1.0.4 (2.0.0対応中...)

🎨 ざっくりと

  • ApiClientApiRequest が存在する
  • ApiRequestCommand パターンになっており、一つのリクエストに対して一つのクラスを作成している
  • ApiClientApiRequest に保持されている内容に応じて、HTTPリクエストを送信する

🧩 ApiClientとApiRequest

まずは、それぞれのクラスを紹介したいと思います。

ApiClient

abstract class ApiClient {
  Future<T> sendRequest<T>(ApiRequest<T> request);
}

渡されたApiRequest を処理するだけです。

実装ファイルの ApiClientImpl ではそれぞれのリクエストメソッド(後述する HttpMethod) に分けて dio を用いたリクエスト送信処理を行なっています。

ここに関しては特殊なことはないと思うので割愛します。

ApiRequest

俗に言うCommandパターンの基底クラスになっています。

また、HttpMethod は独自に作成した4つのリクエストメソッド(GET,POSTなど)を表す enum です。

import 'package:dio/dio.dart';

abstract class ApiRequest<T> {
  String get path;
  HttpMethod get method;
  Map<String, dynamic> get parameters;
  Map<String, dynamic> get data;
  T parse(Response<dynamic> response);

  @override
  String toString() {
    return '''{
      path: $path,
      method: $method
      parameters: $parameters,
      data: $data,
      ''';
  }
}

利用例

空想のAPI https://sample_api.com/api/v1/users を使ってユーザ一覧を取得するコードを書いてみましょう。

まず、ApiRequest を継承した UserListFetchRequest を作成します。

class UserListFetchRequest extends ApiRequest<List<User>> {
  UserListFetchRequest();

  @override
  String get path => 'users';

  @override
  HttpMethod get method => HttpMethod.get;

  @override
  Map<String, dynamic> get parameters => <String, dynamic>{};

  @override
  Map<String, dynamic> get data => <String, dynamic>{};

  List<User> parse(Response<dynamic> response) {
    final usersJson = response.data as Map<String, dynamic>;
    return [...usersJson.map(User.fromJson)];
  }
}

利用側はこんな感じです。

シンプルなリクエス

final users = ref.read(apiClientLocator).sendRequest(
  UserListFetchRequest(),
);

後は煮るなり焼くなり自由にしてください、と言う感じですね。

弊社流にやるのであれば上記のような処理を UserFacade で行い、 usersUserStore などを利用して Provider に保持すると言った実装になります。

気になった方もいらっしゃるかもしれませんが、ドメインや全体共通のパスなどに関しては
ApiClient 側にまとめてしまっています。

引数あり

次に、空想のAPI https://sample_api.com/api/v1/users/{user_id} を使って個別のユーザ情報を取得するリクエスUserFetchRequest を書いてみましょう。

class UserFetchRequest extends ApiRequest<User> {
  UserFetchRequest(this.userId);

  final String userId;

  @override
  String get path => 'users/$userId';

  @override
  HttpMethod get method => HttpMethod.get;

  @override
  Map<String, dynamic> get parameters => <String, dynamic>{};

  @override
  Map<String, dynamic> get data => <String, dynamic>{};
  
  @override
  User parse(Response<dynamic> response) {
    final userJson = response.data as Map<String, dynamic>;
    return User.fromJson(userJson);
  }
}

レスポンスなし

最後に、主に POST の際に多いと思うのですが、何かを送りつけて特にレスポンスのパースが 必要ない場合を考えてみます。

そう言う時のために、 NoResponseApiRequest と言うクラスを用意しています。

abstract class NoResponseApiRequest extends ApiRequest<Void> {
  @override
  Void parse(Response<dynamic> response) {
    return Void.instance;
  }
}

これを継承することで、 parse 処理を書く必要がなくなります。

例えば、空想のAPI https://sample_api.com/api/v1/users/report を使ってユーザを通報するリクエスUserReportRequest を書いてみましょう。

class UserReportRequest extends NoResponseApiRequest {
  UserReportRequest(this.targetUserId);

  final targetUserId;

  @override
  String get path => 'users/report';

  @override
  HttpMethod get method => HttpMethod.post;

  @override
  Map<String, dynamic> get parameters => <String, dynamic>{};

  @override
  Map<String, dynamic> get data => <String, dynamic>{
    'target_user_id': targetUserId,
  };
}

テストコード

さて、最後にテストコードについてまとめて終わりたいと思います。

ApiClient を継承した FakeApiClientImpl を作成し、特定のリクエストに対して特定のレスポンスを返すように書いています。

FakeApiClientImpl

typedef FakeApiResponder = dynamic Function(ApiRequest<dynamic>);

class FakeApiClientImpl extends ApiClient { 
  FakeApiClientImpl._(
    List<FakeApiResponder> responders = const [],
  ): _responders = responders;

  factory FakeAPiClient.create() {
    return FakeApiClientImpl._();
  }

  final List<FakeApiResponder> _responders;

  FakeApiClientImpl appendingFactory<T extends ApiRequest<dynamic>>({
    required FakeApiResponder responder,
  }) {
    return FakeApiClientImpl._(
      responders: [..._responders, responder],
    );
  }

  ...

  @override
  Future<T> sendRequest<T>(ApiRequest<T> request) async {
    // 登録されたresponderで今回のリクエストに対応するレスポンスがあるか確認
    for (final responder in responders) {
      final dynamic response = responder(request);
      if (response is T) {
        return response;
      }
    }

    throw UnimplementedError(...);
  }
}

利用側

void main() {
  final baseApiClient = FakeApiClientImpl();

  final user1 = User(...);
  final user2 = User(...);
  final users = [user1, user2];

  test('api test', () {
    final apiClient = baseApiClient.appendingFactory<UserListFetchRequest>(
      (request) => users,
    );

    final container = ProviderContainer(
      overrides: [
        apiClientLocator.overriderWithValue(apiClient),
      ],
    );

    // 通信処理を含むテスト
  });
}

🎥 さいごに

いかがでしたでしょうか? 少しでも何かの参考になれば幸いです。

エンジニア募集中っ!

YOUTRUSTでは、エンジニアを募集しています! YOUTRUSTに興味を持っていただけた方は下のリンクよりご応募お待ちしておりまっす!

herp.careers