🌊

http + freezed + json_serializableでサクッとイミュータブルなクラスのリポジトリ層を作る

2023/03/12に公開

概要

モバイルアプリに必須ともいえるAPI通信。
また、Flutter(dart)はオブジェクト指向の言語ということもあり、各オブジェクトを生成してアプリを構築していきます。
今回はAPI通信+Immutableなクラスをサクッと作ってリポジトリ層を作っていきます。
※作成したリポジトリ
https://github.com/sukekyo000/freezed_repository

今回作成したリポジトリでは、Flutterではおなじみriverpodのcontributorsのリストを返すGithubApiを使用します。
※APIの仕様書
https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repository-contributors
※実際のAPI
https://api.github.com/repos/rrousselGit/riverpod/contributors?per_page=5

また、今回使用するAPIのGithubのリポジトリとオブジェクト指向におおけるリポジトリ層で混乱するかと思うので、「リポジトリ」をGithubのリポジトリ、「リポジトリ層」をオブジェクト指向におけるリポジトリ層という意味にするので、ご注意ください。

前提条件

・Flutter(dart)の基本的な書き方が分かる
・httpの基本的な書き方が分かる
・freezedの基本的な書き方が分かる
・json_serializableの基本的な書き方が分かる
丁寧に解説していきますが、一度でもhttp・freezed・json_serializableを触ったことがある方が理解が早いかと思います。

バージョン

開発時のバージョンです
・Flutter 3.3.8
https://github.com/sukekyo000/freezed_repository/blob/main/pubspec.yaml
※freezed公式パッケージ
https://pub.dev/packages/freezed
※json_serializable公式パッケージ
https://pub.dev/packages/json_serializable
※http公式パッケージ
https://pub.dev/packages/http

この記事で伝えたいこと

・リポジトリ層でイミュータブルなクラスを生成でき、分かりやすく・素早くコードが書ける

実装

1.http通信用のクラスを作成

https://github.com/sukekyo000/freezed_repository/blob/main/lib/data.dart
まずはhttp通信をするために必要なクラスを生成します。
このクラスを作ることで、
・各ホストのインスタンスを生成できる
・同じホスト・別パスのリポジトリ層の生成が簡単になる
・各ホストのAPIキーの管理できる(今回はAPIキーが必要ないので記載はされていない)
といったことが可能になります。

2.リポジトリ層を作成_1(リクエストクラスを作成)

https://github.com/sukekyo000/freezed_repository/blob/main/lib/repository_contributor_repository.dart#L36-L46

まずはRepositoryContributorsRequestクラスを作ります。仕様書の「Query parameters」にもある通り、「anon」「per_page」「page」がパラメータ名になります。

(name: 'per_page')

と記載することで、dartにおいてはperPageとローワーキャメルケースで扱える一方で、Jsonに変換する際はper_pageと、APIの仕様にあったものに変えることができます。
これが

print(RepositoryContributorsRequest(perPage: "5"));
// 出力値:RepositoryContributorsRequest(anon: null, perPage: 5, page: null)

こうなる

print(RepositoryContributorsRequest(perPage: "5").toJson());
// 出力値:{anon: null, per_page: 5, page: null}

パラメータの「anon」や「page」は@JsonKeyを付ける必要はないですが、仕様と合っているか一目で理解したいので、筆者は全て付けています。

3.リポジトリ層を作成_2(レスポンスクラスを作成)

https://github.com/sukekyo000/freezed_repository/blob/main/lib/repository_contributor_repository.dart#L48-L74

次にRepositoryContributorsResponseクラスを作ります。
ここで重要なのは、json_serializableで「fromJson()」を作成することです。「fromJson()」を生成しておくことで、httpで帰ってきたJsonレスポンスを一発でRepositoryContributorsResponse型に変換できます。
これが

  Future<List<RepositoryContributorsResponse>> getGithubRepositoryContributor(RepositoryContributorsRequest repositoryContributorsRequest) async {
    Response response = await _api.githubApi("repos/rrousselGit/riverpod/contributors", repositoryContributorsRequest.toJson());

    if(response.statusCode == 200){
      List<dynamic> responseJson = jsonDecode(response.body);

      List<RepositoryContributorsResponse> repositoryContributorsResponseList = [];

      for(dynamic repositoryContributorsResponseJson in responseJson){
        print(repositoryContributorsResponseJson);
        // 出力値:{login: GitGud31, id: 26169532, node_id: MDQ6VXNlcjI2MTY5NTMy, avatar_url: https://avatars.githubusercontent.com/u/26169532?v=4, gravatar_id: , url: https://api.github.com/users/GitGud31, html_url: https://github.com/GitGud31, followers_url: https://api.github.com/users/GitGud31/followers, following_url: https://api.github.com/users/GitGud31/following{/other_user}, gists_url: https://api.github.com/users/GitGud31/gists{/gist_id}, starred_url: https://api.github.com/users/GitGud31/starred{/owner}{/repo}, subscriptions_url: https://api.github.com/users/GitGud31/subscriptions, organizations_url: https://api.github.com/users/GitGud31/orgs, repos_url: https://api.github.com/users/GitGud31/repos, events_url: https://api.github.com/users/GitGud31/events{/privacy}, received_events_url: https://api.github.com/users/GitGud31/received_events, type: User, site_admin: false, contributions: 9}

        repositoryContributorsResponseList.add(RepositoryContributorsResponse.fromJson(repositoryContributorsResponseJson));
      }

      return repositoryContributorsResponseList;
    } else {
      return errorResponse;
    }
  }

こうなる

  Future<List<RepositoryContributorsResponse>> getGithubRepositoryContributor(RepositoryContributorsRequest repositoryContributorsRequest) async {
    Response response = await _api.githubApi("repos/rrousselGit/riverpod/contributors", repositoryContributorsRequest.toJson());

    if(response.statusCode == 200){
      List<dynamic> responseJson = jsonDecode(response.body);

      List<RepositoryContributorsResponse> repositoryContributorsResponseList = [];

      for(dynamic repositoryContributorsResponseJson in responseJson){
        print(RepositoryContributorsResponse.fromJson(repositoryContributorsResponseJson));
        // 出力値:RepositoryContributorsResponse(anon: GitGud31, id: 26169532, nodeId: MDQ6VXNlcjI2MTY5NTMy, avatarUrl: https://avatars.githubusercontent.com/u/26169532?v=4, gravatarId: , url: https://api.github.com/users/GitGud31, htmlUrl: https://github.com/GitGud31, followersUrl: https://api.github.com/users/GitGud31/followers, followingUrl: https://api.github.com/users/GitGud31/following{/other_user}, gistsUrl: https://api.github.com/users/GitGud31/gists{/gist_id}, starredUrl: https://api.github.com/users/GitGud31/starred{/owner}{/repo}, subscriptionsUrl: https://api.github.com/users/GitGud31/subscriptions, organizationsUrl: https://api.github.com/users/GitGud31/orgs, reposUrl: https://api.github.com/users/GitGud31/repos, eventsUrl: https://api.github.com/users/GitGud31/events{/privacy}, receivedEventsUrl: https://api.github.com/users/GitGud31/received_events, type: User, siteAdmin: false, contributions: 9)

        repositoryContributorsResponseList.add(RepositoryContributorsResponse.fromJson(repositoryContributorsResponseJson));
      }

      return repositoryContributorsResponseList;
    } else {
      return errorResponse;
    }
  }
}

4.その他のオブジェクトと連携する

ここからは今回の本題とはそれますが、リポジトリ層が完成したので、このレスポンスを元にアプリを構築しやすくなりました。
この作成したオブジェクトを使ってアプリを構築するのも今後執筆できればと思います。

まとめ

今回執筆したことが使えるようになると、わざわざJson↔オブジェクトの関数を作らずに済み、コードもすっきりします。
何よりイミュータブルなオブジェクなので、よりセキュアになります。

間違っている点や質問があれば、是非コメントいただきたいです!

Discussion