LoginSignup
1
0

More than 1 year has passed since last update.

Flutter : chopperでの通信基盤作成

Last updated at Posted at 2022-04-07

こちらも合わせてご覧ください

Flutter通信ライブラリ選定 ~ 選ばれたのはRetrofitでした ~

簡単な特徴

  1. 新しいAPIごとに作成する必要があるのは、
    ・Repositoryのクラス(メソッド)
    ・RequestParameterクラス
    ・Response受け取るクラス
  2. Converterで無理やりconvertしてあげなきゃいけない
  3. httpのラッパーライブラリ

実装

特徴を書くよりもコードを書いた方がわかりやすいと思うので、
RepositoryからAPIを呼び出しレスポンスオブジェクトを返すところまで記載します。

前提
  1. pubspec.yamlにfreezedfreezed_annotationjson_serializablebuild_runnerを固定で入れておいてください。
  2. 叩くAPIとしては、下記のようなログインAPIを例として挙げます
    ・Url → https://domain-hoge.com/fuga/piyo/login
    ・Method → POST
    ・Header → { Content-Type: "applicatioin/json", "piyo": "hogehoge" }
    ・BodyParameter → { id: String, pass: String}
    ・ResponseJson → { base: { bar: String, toto: String } }(レスポンスにbaseなどのキーが固定で入っている場合を考慮)
  3. chopperをpubspec.yamlに追記する

具体的実装

クライアント生成 必要なInterceptorとConvertor

client.dart
class Client extends ChopperClient {
  KDClient()
      : super(converter: CustomConvertor(), interceptors: [CustomHeadersInterceptor.create()]);

  @override
  String get baseUrl => "https://domain-hoge.com";
}

class CustomConvertor implements Converter {
  @override
  Request convertRequest(Request request) {
    final req = applyHeader(
      request,
      contentTypeKey,
      jsonHeaders,
      override: false,
    );

    return encodeJson(req);
  }

  @override
  FutureOr<Response<BodyType>> convertResponse<BodyType, InnerType>(
      Response response) {
    return decodeJson<BodyType, InnerType>(response);
  }

  Request encodeJson(Request req) {
    var contentType = req.headers[contentTypeKey];
    if (contentType != null && contentType.contains(jsonHeaders)) {
      return req.copyWith(body: json.encode(req.body));
    }
    return req;
  }

  Response<BodyType> decodeJson<BodyType, InnerType>(Response response) {
    var contentType = response.headers[contentTypeKey];
    var body = response.body;
    if (contentType != null && contentType.contains(jsonHeaders)) {
      body = utf8.decode(response.bodyBytes);
    }
    try {
      var mapData = json.decode(body);
      var responseBase = mapData["base"];
      BodyType bodyType;
      switch (BodyType) {
        case LoginResponse:
          bodyType = LoginResponse.fromJson(data as Map<String, dynamic>) as BodyType;
          break;
        case String:
          bodyType = responseBase.data as BodyType;
          break;
        default:
          bodyType = responseBase as BodyType;
      }

      return response.copyWith<BodyType>(body: bodyType);
    } catch (e) {
      chopperLogger.warning(e);
      return response.copyWith<BodyType>(body: body);
    }
  }
}

RepositoryからClient.callを呼び出す

login_repository.dart
abstract class LoginDataSource {
  Future<Result<LoginResponse>> login(LoginModel loginModel);
}

class LoginRepository extends LoginDataSource {
  LoginRepository() : service = AuthService.create(Client());
  AuthService service;

  @override
  Future<Result<LoginResponse>> login(LoginModel loginModel) async {
    final parameter = LoginRequestParameter(
        id: loginModel.id, pass: loginModel.pass);
    try {
      final response = await service.login(parameter);
      return Result.success(response.body!);
    } catch (e) {
      return Result.failure();
    }
  }
}

リクエストを叩くChopperServiceクラスの作成
下記クラスを作成したらgenerateしましょう

auth_service.dart
part 'auth_service.chopper.dart';

@ChopperApi(baseUrl: "/fuga/piyo")
abstract class AuthService extends ChopperService {
  static AuthService create([ChopperClient? client]) => _$AuthService(client);

  @Post(path: "/login")
  Future<Response<LoginResponse>> login(@Body() LoginRequestParameter parameter);
}

リクエスト等の基幹クラス

http_request_base.dart
// リクエストの基幹
abstract class HttpRequestBase {
  HttpMethod get apiMethod;
  String get path;
  RequestParameter? get parameter;

  String get method => apiMethod.name.toUpperCase();

  Uri get url {
    if (apiMethod == HttpMethod.get && parameter != null) {
      return Uri.https("domain-hoge.com", path, parameter!.toJson());
    }
    return Uri.https("domain-hoge.com", path);
  }

  Map<String, String> get headers => { Content-Type: "applicatioin/json", "piyo": "hogehoge" };
}

enum HttpMethod {
  post,
  get,
  put,
  patch,
  delete
}

リザルトクラス(上記同様にflutter pub run build_runner build ~ する)

result.dart
part 'result.freezed.dart';

@freezed
class Result<T> with _$Result<T> {
  const factory Result.success(T value) = Success<T>;
  const factory Result.failure() = Failure<T>;
}

ログインリクエスト

login_request.dart
class LoginRequest extends HttpRequestBase {
  LoginRequest(this.loginParameter) : super();
  
  final LoginRequestParameter loginParameter;
  
  @override
  HttpMethod get apiMethod => HttpMethod.post;

  @override
  String get path => "/fuga/piyo/login";

  @override
  RequestParameter? get parameter => loginParameter;
}

ログインリクエストのパラメータ(freezedなので下記ファイルを作成したら、$flutter pub run build_runner build --delete-conflicting-outputsを実行する)

login_request_parameter.dart
part 'login_request_parameter.freezed.dart';
part 'login_request_parameter.g.dart';

@freezed
class LoginRequestParameter with _$LoginRequestParameter {
  factory LoginRequestParameter({
    required String id,
    required String pass,
  }) = _LoginRequestParameter;

  factory LoginRequestParameter.fromJson(Map<String, dynamic> json) => _$LoginRequestParameterFromJson(json);
}

レスポンス受け取るクラス(上記同様にflutter pub run build_runner build ~ する)

login_response.dart
part 'login_response.freezed.dart';
part 'login_response.g.dart';

@freezed
class LoginResponse with _$LoginResponse {
  factory LoginResponse({
    required String bar,
    required String toto,
  }) = _LoginResponse;

  factory LoginResponse.fromJson(Map<String, dynamic> json) => _$LoginResponseFromJson(json);
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0