• トップ
  • ブログ一覧
  • 【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜前編・API通信の実装〜
  • 【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜前編・API通信の実装〜

    こー(エンジニア)こー(エンジニア)
    2021.11.09

    IT技術

    はじめに

    クロスプラットフォームとしてますます盛り上がりを見せている Flutter

    「これから始めてみようかな?」と、公式チュートリアルにあるTODOアプリなどを作ってみた方も多いのではないでしょうか?

    一通り雰囲気を掴めてきたところで、アプリ開発の次のステップアップとして欠かせないのが API 通信ですね。

    Retrofit という代表的な API クライアントライブラリを用いて簡単なアプリケーションを作ることで、 Flutter での API 通信の基礎を学んでいきたいと思います!

    開発環境は構築が完了している前提で進めますので、まだの方は Flutter 公式ページを参考に環境構築を済ませてからご覧ください。

    作成するもの

    公開されている Qiita の API を使って、「Qiitaの記事一覧を表示」「実際の記事を表示」するアプリを作っていきます!

      アーキテクチャは現在のアプリ開発の主流になりつつある MVVM(Model-View-ViewModel) を採用します。

      前編ではプロジェクト作成からデータモデルと API 通信周りの実装、後編では ViewModel の実装と画面(View)側、と二部構成で進めていきます。

      プロジェクトの作成

      まずは、作るアプリのプロジェクトを作成します。

      ターミナルを開いて、好きなディレクトリに移動して、以下コマンドを実行してください。

      1flutter create flutter_qiita_retrofit

      指定したディレクトリに flutter_qiita_retrofit のプロジェクトが作成されていると思うので、Android Studio で開きましょう。

      データモデルの実装

      API 通信で取得されるデータを格納するモデルクラスを実装していきます。

      パッケージの準備

      Qiita の API 通信では、データは Json 形式でやり取りされるため、アプリ内で扱えるように Json をモデルクラスに変換、またはその逆にモデルクラスから Json に変換する必要があります。

      この変換のことをパースと呼びます。

      今回は json_serializable というパッケージを利用して、Json⇄モデルのパース部分を自動生成していきます。

      json_serializable とそのアノテーションを利用するための json_annotation、コードを自動生成するための build_runnner 、の3つのパッケージを追加します。

      pubspec.yaml に以下のように記述しましょう。

      1dependencies:
      2  # 〜省略〜
      3  json_annotation:    # 追加
      4
      5dev_dependencies:
      6  # 〜省略〜
      7  build_runner:       # 追加
      8  json_serializable:  # 追加

      そして、ターミナルからプロジェクト直下で以下コマンドを実行してパッケージをインストールします。

      1flutter pub get

      これで完了です!

      User(ユーザー情報)モデルの実装

      記事の著者であるユーザー情報を扱うための User モデルを実装します。

      model ディレクトリを作成後、その中に user.dart ファイルを作成して、以下のように記述しましょう。

      1import 'package:json_annotation/json_annotation.dart';
      2
      3part 'user.g.dart';
      4
      5@JsonSerializable(fieldRename: FieldRename.snake)
      6class User {
      7  String id;
      8  String profileImageUrl;
      9
      10  User({required this.id, required this.profileImageUrl});
      11
      12  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
      13
      14  Map<String, dynamic> toJson() => _$UserToJson(this);
      15}

      この時点では、コードにエラーの赤線が引かれてると思いますが、気にせず進めてください。

      5行目の fieldRename: FieldRename.snake はキャメルケースで書かれているプロパティを、Json で対応するプロパティ名を生成する際に自動でスネークケースに変換するための指定です。

      Article(記事)モデルの実装

      実際の記事データを格納する Article モデルを実装します。

      こちらも model ディレクトリに article.dart ファイルを作成して、以下のように記述します。

      1import 'package:flutter_qiita_retrofit/model/user.dart';
      2import 'package:json_annotation/json_annotation.dart';
      3
      4part 'article.g.dart';
      5
      6@JsonSerializable(explicitToJson: true)
      7class Article {
      8  String id;
      9  String title;
      10  String url;
      11  User user;
      12
      13  Article(
      14      {required this.id,
      15      required this.title,
      16      required this.url,
      17      required this.user});
      18
      19  factory Article.fromJson(Map<String, dynamic> json) =>
      20      _$ArticleFromJson(json);
      21
      22  Map<String, dynamic> toJson() => _$ArticleToJson(this);
      23}

      User モデルと同じように、コードにエラーの赤線が引かれますが、こちらも気にせず進めましょう。

      パース処理の自動生成

      それでは build_runner を使って、今書いた User、Article モデルのパース部分のコードを自動生成しましょう!

      ターミナルを開いて、以下のコマンドを入力してください。

      1flutter pub run build_runner build

      すると、 user.g.dart、 article.g.dart ファイルが同ディレクトリに生成されているはずです。

      生成されたコードをそれぞれ確認してみましょう。

      1// GENERATED CODE - DO NOT MODIFY BY HAND
      2
      3part of 'user.dart';
      4
      5// **************************************************************************
      6// JsonSerializableGenerator
      7// **************************************************************************
      8
      9User _$UserFromJson(Map<String, dynamic> json) {
      10  return User(
      11    id: json['id'] as String,
      12    profileImageUrl: json['profile_image_url'] as String,
      13  );
      14}
      15
      16Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
      17      'id': instance.id,
      18      'profile_image_url': instance.profileImageUrl,
      19    };
      1// GENERATED CODE - DO NOT MODIFY BY HAND
      2
      3part of 'article.dart';
      4
      5// **************************************************************************
      6// JsonSerializableGenerator
      7// **************************************************************************
      8
      9Article _$ArticleFromJson(Map<String, dynamic> json) {
      10  return Article(
      11    id: json['id'] as String,
      12    title: json['title'] as String,
      13    url: json['url'] as String,
      14    user: User.fromJson(json['user'] as Map<String, dynamic>),
      15  );
      16}
      17
      18Map<String, dynamic> _$ArticleToJson(Article instance) => <String, dynamic>{
      19      'id': instance.id,
      20      'title': instance.title,
      21      'url': instance.url,
      22      'user': instance.user.toJson(),
      23    };

      それぞれ、 ~FromJson~ToJson というメソッドが生成されていて、ここで Json ⇄ モデル の変換が行われます。

      これで、モデルクラスのエラーの赤線も消えているはずなので、確認しましょう!

      API 通信周りの実装

      それでは、この記事の本題 Retrofit を使って API 通信周りを実装していきます!

      パッケージの準備

      API 通信周りに必要なライブラリを追加していきます。

      pubspec.yaml に、さらに以下を追記しましょう。

      1dependencies:
      2  # ~省略~
      3  retrofit:
      4  dio:
      5
      6dev_dependencies:
      7  # ~省略~
      8  retrofit_generator:

      Retrofit は、 Dio という先行している API クライアントパッケージを発展させたラッピングパッケージです。

      そのため、 Retrofit で扱われるクラスは Dio を利用したものが含まれていますので追加します。

      追加したら、以下コマンドでパッケージをインストールしましょう。

      1flutter pub get

      APIクライアントの実装

      それでは、いよいよ Retrofit を利用した API クライアントクラスを実装しましょう!

      今回は記事一覧取得の API(https://qiita.com/api/v2/items)を利用します。

      client ディレクトリを作成し、api_client.dart ファイルを作成して、以下のように記述します。

      1import 'package:dio/dio.dart';
      2import 'package:flutter_qiita_retrofit/article.dart';
      3import 'package:retrofit/http.dart';
      4
      5part 'api_client.g.dart';
      6
      7@RestApi(baseUrl: "https://qiita.com/api/v2")
      8abstract class ApiClient {
      9  factory ApiClient(Dio dio, {String baseUrl}) = _ApiClient;
      10
      11  @GET("/items")
      12  Future<List<Article>> fetchArticles();
      13}

      そして、ターミナルを開いて以下のコマンドを入力します。

      1flutter pub run build_runner build

      すると、 api_client.g.dart ファイルが自動生成されます。

      中身をのぞいてみましょう。

      1// GENERATED CODE - DO NOT MODIFY BY HAND
      2
      3part of 'api_client.dart';
      4
      5// **************************************************************************
      6// RetrofitGenerator
      7// **************************************************************************
      8
      9class _ApiClient implements ApiClient {
      10  _ApiClient(this._dio, {this.baseUrl}) {
      11    baseUrl ??= 'https://qiita.com/api/v2';
      12  }
      13
      14  final Dio _dio;
      15
      16  String? baseUrl;
      17
      18  @override
      19  Future<List<Article>> fetchArticles() async {
      20    const _extra = <String, dynamic>{};
      21    final queryParameters = <String, dynamic>{};
      22    final _data = <String, dynamic>{};
      23    final _result = await _dio.fetch<List<dynamic>>(
      24        _setStreamType<List<Article>>(
      25            Options(method: 'GET', headers: <String, dynamic>{}, extra: _extra)
      26                .compose(_dio.options, '/items',
      27                    queryParameters: queryParameters, data: _data)
      28                .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
      29    var value = _result.data!
      30        .map((dynamic i) => Article.fromJson(i as Map<String, dynamic>))
      31        .toList();
      32    return value;
      33  }
      34
      35  RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
      36    if (T != dynamic &&
      37        !(requestOptions.responseType == ResponseType.bytes ||
      38            requestOptions.responseType == ResponseType.stream)) {
      39      if (T == String) {
      40        requestOptions.responseType = ResponseType.plain;
      41      } else {
      42        requestOptions.responseType = ResponseType.json;
      43      }
      44    }
      45    return requestOptions;
      46  }
      47}

      api_client.dart で記述した抽象クラス ApiClient を継承した _ApiClient クラスが生成されています。

      fetchArticles メソッドが記事一覧取得 API を叩き、レスポンスの Json から Article モデルの配列へのパースを行う処理が実装されていることがわかりますね。

      このように Retrofit を使うことによって、開発側は API クライアントの雛形を抽象クラスで記述するだけでよく、実装を自動生成に任せることができます。

      実装部分に関するヒューマンエラーが減らせますし、抽象クラスでリクエストタイプ・パス・規定URLを全て一元して管理できるのでとても便利です。

      「ちょっと難しいな...」と思う方は、「モデルの実装とAPIクライアントの抽象クラスを書けば、通信周りの実装は自動でやってくれるんだ!」とざっくり理解していただければ大丈夫です!

      リポジトリクラスの実装

      API クライアントの実装が完了しましたが、後編で触れる画面(View)側のコードで直接このAPIクライアントを呼び出すわけではありません。

      採用する MVVM アーキテクチャで、 「View 側は画面処理に徹するべき」という原則があるため、API クライアントを直接呼び出してしまうと、エラーハンドリングなども View 側で担ってしまい責務超過ことになってしまうからです。

      では、API クライアントはどこから呼び出すのでしょうか?

      それが今から実装していくリポジトリクラスです。

      リポジトリクラスは、実際のデータアクセスを取り扱うクラスです。

      View 側ではこのリポジトリクラスを使って、データの取得やリクエストの送信を行います。

      レスポンスクラスの実装

      まずは、リポジトリクラスで API クライアントから返されたレスポンスを扱いやすくするためのレスポンスクラスを実装していきます。

      ここでは freezed というパッケージを利用します。

      pubspec.yaml に以下を追記しましょう。

      1dependencies:
      2  # ~省略~
      3  freezed:
      4
      5dev_dependencies:
      6  # -省略-
      7  freezed_annotation:

      これまでと同様に、以下のコマンドでライブラリをインストールします。

      1flutter pub get

      インストールできたら、 response ディレクトリを作成し、その中に result.dart ファイルを作成して、以下のようにレスポンスクラスの雛形を記述しましょう。

      1import 'package:dio/dio.dart';
      2import 'package:freezed_annotation/freezed_annotation.dart';
      3
      4part 'result.freezed.dart';
      5
      6@freezed
      7abstract class Result<T> with _$Result<T> {
      8  const factory Result.success(T value) = Success<T>;
      9  const factory Result.failure(DioError error) = Failure<T>;
      10}

      こちらもモデルクラスと同様にエラーの赤線が引かれていると思いますが、気にせず進みましょう。

      このクラスの実装もモデルやAPIクライアントと同じように自動生成するので、ターミナルを開いて以下のコマンドを実行します。

      1flutter pub run build_runner build

      すると、 result.freezed.dart ファイルが生成されているが分かると思います。

      このクラスを定義することで、レスポンスは全て Result クラスに属しながら、成功時は Success クラス、エラー時は Failure クラス とそれぞれ別クラスとしても扱うことができます。

      レスポンスを受け取った側は、 Success クラスなのか Failure クラスなのか、クラス判定で処理を分けることができるようになるわけですね。

      これだけ聞くと、「普通のクラス継承と何が違うんだ?」と疑問に感じる方もいらっしゃると思いますが、普通のクラス継承と違うところは、各々のクラスは別々のプロパティを持つことができる、という点です。

      Android 開発経験者には 「Kotlin における sealed class のような使い方ができる」と言ったら分かりやすいでしょうか。

      T の部分には、受け取るデータの型が入り、 Result<T> という形でラップして使っていきます。

      Success クラスには、受け取ったデータ型の T が入り、 Failure クラスには通信時エラーの DioError クラスが入るように設計しています。

      自動生成された継承クラス(result.freezed.dart)については、本題と脱線してしまうので割愛しますが、興味がある方はのぞいてみてくださいね!

      リポジトリクラス: 抽象クラスの実装

      それでは、いよいよリポジトリクラスを作っていきます!

      まずは抽象クラスです。

      repository ディレクトリを作成し、その中に article_repository.dart ファイルを作成して、以下のように記述してください。

      1import 'package:flutter_qiita_retrofit/model/article.dart';
      2import 'package:flutter_qiita_retrofit/response/result.dart';
      3
      4abstract class ArticleRepository {
      5  Future<Result<List<Article>>> fetchArticles();
      6}

      5行目で Result クラスを Future というクラスでラッピングしていますが、これは非同期でのデータのやり取りを扱うクラスですね。

      アプリ内に設定などを保存するための flutter_secure_storage や、アプリ内データベースの moor など、便利なパッケージでも頻繁に登場するので名前だけでも覚えて帰りましょう!

      実際の使い方は、次の継承クラスの実装で紹介します。

      リポジトリクラス: 継承クラスの実装

      次に、その継承クラスを実装します。

      repository ディレクトリ内に、article_repository_impl.dart ファイルを作成し、以下のように記述しましょう。

      1import 'package:dio/dio.dart';
      2import 'package:flutter_qiita_retrofit/client/api_client.dart';
      3import 'package:flutter_qiita_retrofit/model/article.dart';
      4import 'package:flutter_qiita_retrofit/repository/article_repository.dart';
      5import 'package:flutter_qiita_retrofit/response/result.dart';
      6
      7class ArticleRepositoryImpl with ArticleRepository {
      8  final ApiClient _client;
      9
      10  ArticleRepositoryImpl([ApiClient? client])
      11      : _client = client ?? ApiClient(Dio());
      12
      13  @override
      14  Future<Result<List<Article>>> fetchArticles() {
      15    return _client
      16        .fetchArticles()
      17        .then((articles) => Result<List<Article>>.success(articles))
      18        .catchError((error) => Result<List<Article>>.failure(error));
      19  }
      20}

       

      fetchArticle メソッドの実装について説明します。

      1  @override
      2  Future<Result<List<Article>>> fetchArticles() {
      3    return _client
      4        .fetchArticles()
      5        .then((articles) => Result<List<Article>>.success(articles))
      6        .catchError((error) => Result<List<Article>>.failure(error));
      7  }

      then メソッドは成功時で、記事モデルの配列(List<Article>)が流れてきますので、先ほど実装したレスポンスクラス Result の success メソッドに流し、 Success を返します。

      catchError メソッドは失敗時で、実際のエラー(DioError)が流れてきますので、同じく Result クラスの failure メソッドに流し、 Failure を返します。

      これで Qiita から記事の一覧を取得する API 通信周りの準備が整いました!

      前編まとめ

      前編では、プロジェクト作成から、 Qiita の記事一覧取得 API 通信の実装を行いました。

      前編は View 以外の裏側の処理の実装だったので、文字だらけになってしまい疲れてしまった人もいるのではないでしょうか(笑)

      後編では、実装した記事一覧取得 API を利用して、記事一覧表示と WebView を使った記事詳細表示の実装をしていきます!

      後編はこちら!

      【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜後編・ViewModelと画面の実装〜2021.11.10【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜後編・ViewModelと画面の実装〜はじめにこの記事は、 【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜前編・API通信の実装〜 ...

      こー(エンジニア)

      こー(エンジニア)

      おすすめ記事

      GitHubActionsのランナーに触れてみた

      こやまん(エンジニア)

      こやまん(エンジニア)

      2024.03.28

      IT技術

      Azure Data FactoryでSlackへ通知をしてみる

      たかやん(エンジニア)

      たかやん(エンジニア)

      2024.03.28

      IT技術

      GCP Secret Managerを使ってみた

      たなゆー(エンジニア)

      たなゆー(エンジニア)

      2024.03.21

      IT技術

      Bitriseのパイプラインと環境変数

      加納(エンジニア)

      加納(エンジニア)

      2024.03.11

      IT技術