もくじ
こんにちは。
スマホアプリをメインに開発しているロッキーカナイです。
今回、Flutterの通信ライブラリRetrofitを使ってみたのですがいい感じだったので記事にしました。
これまで公式の通信ライブラリであるHttpしか使ったことなく、自前実装だと結構手を入れる必要があるのがネックかなと思っていたのですが、Retrofitだと超簡単に実装できました。小規模開発や個人開発であればお勧めしたいライブラリの一つです。
Retrofitとは
Retrofitは、通信ライブラリのDioをラッピングしたライブラリです。
Androidでも同様のライブラリがあり、これのFlutter版になるようです。リクエストクラスを自動生成してくれるので、自作しなくてよいという大変便利な代物です。
参考サイト
【Flutter】Retrofitを使ってAPI通信をしてみよう! 〜前編・API通信の実装〜
Flutter通信ライブラリ選定 ~ 選ばれたのはRetrofitでした ~
作るもの
今回はWebAPIのテストに使えるWebAPIサーバーhttpbinを利用します。
参考サイト
APIクライアント開発時のモックに使えるhttpbinの紹介
- Retrofit(+Dio)でhttpbinへリクエストを投げる(Retrofit(+Dio)を利用 +freezedで生成)
- レスポンスを受け取る(データモデルはfreezedで生成)
- 成功・失敗に応じてログを出力する(ログはloggerを利用)
実装
Flutterバージョン
Flutter (Channel stable, 3.7.9)
pubspec.yaml(抜粋)
dependencies:
...
freezed_annotation:
json_annotation: ^4.8.0
retrofit:
dio:
dev_dependencies:
...
freezed: ^2.3.2
build_runner: ^2.3.3
json_serializable:
retrofit_generator:
logger: ^1.3.0
バージョンは使用している環境に合わせてください。
プラグインの取得は、お決まりの
flutter pub get
を行ってください。
レスポンスデータのモデル生成
httpbinからのレスポンスデータのモデルを生成します。
どうやらhttpbinはAPIアクセスしたデータを返してくれるのでそれらを持つクラスにしました。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'test_model.freezed.dart';
part 'test_model.g.dart';
@freezed
class Test with _$Test {
factory Test({
required Map<String, String> args, // パラメータ情報
required Map<String, String> headers, // ヘッダー情報
Map<String, String>? form, // フォーム情報
String? data, // データ情報
Map<String, String>? files, // ファイル情報
required String origin, // アクセス元のIPアドレス
required String url, // URL
}) = _Test;
factory Test.fromJson(Map<String, dynamic> json) => _$TestFromJson(json);
}
JSONデータからこのクラスへがちゃりんこするのでfromJson()関数追加とpartでjson_serializableでの関連ファイルの生成をする記述をしてます。
クラスが用意できたら、生成しますので、
flutter pub run build_runner build --delete-conflicting-outputs
を実行します。同階層に関連ファイルの生成がされればOKです。
WebAPIクライアントクラスの生成(Retrofit)
続いてRetrofitを用いたWebAPIのクライアントクラスを作ります。
今回は、GETとPOSTとエラー系のエンドポイントを叩きたいと思います。
import 'package:dio/dio.dart' hide Headers;// ヘッダーを使用しない場合はhide以降を削除
import 'package:retrofit/http.dart';
import 'package:test_flutter/models/test_model.dart';
part 'test_api_client.g.dart';
@RestApi(baseUrl: "https://httpbin.org") // APIのベースURL
abstract class TestApiClient {
factory TestApiClient(Dio dio, {String baseUrl}) = _TestApiClient;
static const _headers = <String, dynamic>{ // ヘッダー使用例
"Content-Type": "application/json",
"Custom-Header": "Your header"
};
@GET("/get") // HttpメソッドをGET
@Headers(_headers) // ヘッダー情報
Future<Test> getTest(@Query("id") String id); // リクエスト関数の形式を記載
@POST("/post") // HttpメソッドをPOST
@Headers(_headers) // ヘッダー情報
Future<Test> postTest(@Field("id") String id); // リクエスト関数の形式を記載
@GET("/status/404") // HttpメソッドをGET
@Headers(_headers) // ヘッダー情報
Future<Test> error(); // リクエスト関数の形式を記載
}
クラスが用意できたら、生成しますので、
flutter pub run build_runner build --delete-conflicting-outputs
を実行します。同階層に関連ファイルの生成がされればOKです。
レスポンスの成功・失敗を扱いやすくするResultクラスの生成
例えば、notifierクラス等でリポジトリの関数をコールし結果をレスポンスデータ(JSONをオブジェクト化したクラス、ここで紹介しているTestクラス)で受け取るという方法もありますが、もう一段成功と失敗の判別がつく様にResultクラスと継承したSuccessクラスとFailureクラスを用意します。これを分岐し返却することで、結果の判別が容易になります。また、notifierクラス等でこの結果に対しstate更新だけすればよいことになり、大変簡潔です。
import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'result.freezed.dart';
@freezed
abstract class Result<T> with _$Result<T> {
const factory Result.success(T value) = Success<T>;
const factory Result.failure(DioError error) = Failure<T>;
}
クラスが用意できたら、生成しますので、
flutter pub run build_runner build --delete-conflicting-outputs
を実行します。同階層に関連ファイルの生成がされればOKです。
リポジトリクラスの作成
続いてリポジトリクラスの作成をします。
Retrofitを用いて生成されたクラスの呼び出しと結果を実装します。
import 'package:dio/dio.dart';
import 'package:test_flutter/client/test_api_client.dart';
import 'package:test_flutter/models/test_model.dart';
import 'package:test_flutter/response/result.dart';
export 'package:test_flutter/response/result.dart';
abstract class _TestRepository {
Future<Result<Test>> getTest(String id);
Future<Result<Test>> postTest(String id);
Future<Result<Test>> error();
}
class TestRepository implements _TestRepository {
final TestApiClient _client;
TestRepository([TestApiClient? client])
: _client = client ?? TestApiClient(Dio());
@override
Future<Result<Test>> getTest(String id) {
return _client
.getTest(id) // getTest実行
.then((test) => Result<Test>.success(test)) // 成功時
.catchError((error) => Result<Test>.failure(error)); // 失敗時
}
@override
Future<Result<Test>> postTest(String id) {
return _client
.postTest(id) // postTest実行
.then((test) => Result<Test>.success(test)) // 成功時
.catchError((error) => Result<Test>.failure(error)); // 失敗時
}
@override
Future<Result<Test>> error() {
return _client
.error() // error実行
.then((test) => Result<Test>.success(test)) // 成功時
.catchError((error) => Result<Test>.failure(error)); // 失敗時
}
}
リポジトリ呼び出し元クラスの実装
最後に呼び出し元クラス(notifier等)の実装です。
/// 抜粋
final logger = Logger();
Future<void> getTest() async {
final repository = TestRepository();
await repository.getTest("1").then((result) {
result.when(success: (test) {
logger.i("success: $test"); // 成功時
}, failure: (error) { // 失敗時
logger.e("error: ${error.message}");
});
});
}
Future<void> postTest() async {
final repository = TestRepository();
await repository.postTest("1").then((result) {
result.when(success: (test) { // 成功時
logger.i("success: $test");
}, failure: (error) { // 失敗時
logger.e("error: ${error.message}");
});
});
}
Future<void> error() async {
final repository = TestRepository();
await repository.error().then((result) {
result.when(success: (test) { // 成功時
logger.i("success: $test");
}, failure: (error) { // 失敗時
logger.e("error: ${error.message} code: ${error.response?.statusCode}");
});
});
}
結果
結果はLoggerで表示させました。
getTest()
postTest()
error()
ばっちりOK
最後に
標準ライブラリだとどうしても工数がかさむ部分を自動生成してくれるのはすごく魅力的に感じました。
あとはエラー時にサーバーからのエラーメッセージを返してもらって、そのままダイアログで出力すれば通信周りの実装は結構十分事足りるのではと思ってしまってますし、何しろ個人的にも構成が分かりやすかったので気に入りました。
こんな便利なものもあるんだと、関心を持っていただけたら嬉しいです。