【Flutter】Retrofitを使ってHTTPリクエスト(GET)

# 使用パッケージ

https://pub.dev/packages/retrofit

[Dio](https://qiita.com/kenta4327/items/b319108a54d0a8fbf9d0) と [json_serializable](https://qiita.com/kenta4327/items/d58a73cc85b944521ccc) を使ったHTTPリクエストコード部分を自動生成してくれるもの。

叩くAPIは [openweathermap](https://openweathermap.org/) を使わせてもらう。

# 使い方

### pubspec.yaml にパッケージを追加

```
dependencies:
  json_annotation: ^4.8.1
  dio: ^5.1.2
  retrofit: ^4.0.1

dev_dependencies:
  build_runner: ^2.4.4
  json_serializable: ^6.7.0
  retrofit_generator: ^7.0.1
```

### HTTPリクエスト用の抽象クラスを用意

```dart
import 'package:dio/dio.dart';
import 'package:flutter_retrofit/utils/city_weather.dart';
import 'package:retrofit/retrofit.dart';

part 'rest_client.g.dart';

@RestApi(baseUrl: 'https://api.openweathermap.org/data/2.5')
abstract class RestClient {
  factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

  @GET('/weather')
  Future<City> getCityWeather(
    @Query("lat") String lat,
    @Query("lon") String lon,
    @Query("appid") String appid,
    @Query("units") String units,
    @Query("lang") String lang
  );
}
```

見たままではあるが、
`@RestApi()` でベースとなるURLを定義して、
`@GET()` で各APIの定義を行っていく。
`@Query()` でクエリパラメータを記述することも可能。

:warning: 生成後のファイルを参照してる部分もあるのでこの時点でエラーが出てるのは気にせず次に進む

### コード生成

```shell
flutter pub run build_runner build
```

これを叩くことにより

```dart
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'rest_client.dart';

// **************************************************************************
// RetrofitGenerator
// **************************************************************************

// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers

class _RestClient implements RestClient {
  _RestClient(
    this._dio, {
    this.baseUrl,
  }) {
    baseUrl ??= 'https://api.openweathermap.org/data/2.5';
  }

  final Dio _dio;

  String? baseUrl;

  @override
  Future<City> getCityWeather(
    String lat,
    String lon,
    String appid,
    String units,
    String lang,
  ) async {
    const _extra = <String, dynamic>{};
    final queryParameters = <String, dynamic>{
      r'lat': lat,
      r'lon': lon,
      r'appid': appid,
      r'units': units,
      r'lang': lang,
    };
    final _headers = <String, dynamic>{};
    final Map<String, dynamic>? _data = null;
    final _result =
        await _dio.fetch<Map<String, dynamic>>(_setStreamType<City>(Options(
      method: 'GET',
      headers: _headers,
      extra: _extra,
    )
            .compose(
              _dio.options,
              '/weather',
              queryParameters: queryParameters,
              data: _data,
            )
            .copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
    final value = City.fromJson(_result.data!);
    return value;
  }

  RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
    if (T != dynamic &&
        !(requestOptions.responseType == ResponseType.bytes ||
            requestOptions.responseType == ResponseType.stream)) {
      if (T == String) {
        requestOptions.responseType = ResponseType.plain;
      } else {
        requestOptions.responseType = ResponseType.json;
      }
    }
    return requestOptions;
  }
}
```

のようなメソッドが自動生成される。
(端折ってしまったが、getCityWeather()内の `final value = City.fromJson(_result.data!);` で、 `fromJson()`メソッドを使用してレスポンスをクラス形式に変換している。ここの変換にjson_serializableで用意したコードが必要となる。(json_serializableで用意するクラスとしては、特別なものではなく、Retrofitを使用しない場合(Dioをそのまま使用する場合)と同等のもので問題ない))

これを使用して下記のようなコードを書くと結果が取得できる。

```dart
    // 東京タワーの緯度経度
    final lat = '35.6579781';
    final lon = '139.7433048';
    final appid = 'xxxxxxxxxx';
    final units = 'metric';
    final lang = 'ja';

    final dio = Dio();
    final client = RestClient(dio);

    final City city = await client.getCityWeather(lat, lon, appid, units, lang);

    // とりあえず場所と天気だけ表示
    print('CityWeathre: ${city.id}, ${city.name}, ${city.weather?[0].description}');
```

# 使ってみての感想

HTTPリクエスト部分と結果のマッピング部分を、
定義クラスだけ用意すれば自動生成してくれるのは便利。

だが、HTTPアクセスからマッピングまでが
`final City city = await client.getCityWeather(lat, lon, appid, units, lang);`
に隠蔽されているので、
定義するときに一部の記述を間違えたなどでうまく動かないときに、
どこが悪いのかを探すのがちょっと大変。