Type safe http calls in Flutter with Chopper

Type safe http calls in Flutter with Chopper

ยท

12 min read

Type safe HTTP calls in Flutter with Chopper

Problem:

I am not sure about you but for me when I started working with Flutter (or dart) I had great difficulty in handling API calls and JSON responses because of lack of a native JSON support as easy as python or Js (the languages I was previously working with), I mean the data type Map<String, dynamic> is almost equivalent to a JSON but naah. Also, I believe when you are going with a strictly typed language like dart you would like your API responses to also be dart objects rather than a random Map.

Well my current project requires a lot of communication with an Express-based API, initially I was using flutter team's own http package which was really good but adding query parameters and headers in them is terribly handled and in short, I don't like it.

That's when I came across Chopper which is dubbed as retrofit for flutter, this is what I'll be talking about in this blog

Solution: Chopper

Well chopper is a flutter package that helps you simplify your REST call

API/HTTP calls will be handled using chopper

import 'package:chopper/chopper.dart';

// the line below is strictly supposed to be <fileName>.chopper.dart
part 'chopper.chopper.dart';

@ChopperApi(baseUrl: 'api/v1/')
abstract class API extends ChopperService {
  @Get()
  Future<Response> getter();

  @Post()
  Future<Response> post(@Body() Map<String, dynamic> body);

  static API create() {
    final client = ChopperClient(
        baseUrl: 'https://d2def3d9f930.ngrok.io/',
        services: [_$API()],
        converter: JsonConverter());
  }
}

Well, chopper is one those dependencies which generate code for you coooool, that's why we need the part of directive.

The above snippet is the simplest implementation of the chopper packages without any type safety or interceptors. Showing only 2 calls a get and a post request both to https://d2def3d9f930.ngrok.io/api/v1

we can run the following command to build the .chopper.dart file

flutter packages pub run build_runner watch

Your Service should be an abstract class and should extend the ChopperService class

The decorator @ChopperAPI is needed as it defines the abstract class (or interface) as a ChopperService

Every call is a separate function and the method is defined by the corresponding decorator, which takes in a fixe

  • path: the path duh ๐Ÿคท๐Ÿป
  • headers>: A dictionary/Map of headers apart from default ones
Dynamic decorators as parameters

This is followed by a list of parameters to the actual function, these need to specified with suitable decorators like

  • @Path: if you have a variable path as in you send a user id or order id in your path itself you can use this directive

      @Get(path: '/hashnode/{user}')
      Future<Response> getUser(@Path String userId);
    
  • @Query: To send query parameters with the call

      @Get(path: '/hashnode/iresharma')
      Future<Response> getUser(@Query('search') String searchString);
    
  • @QueryMap: To send a series of Query parameters you can use this directive to pass a Map which will be converted to multiple queries

      @Get(path: '/hashnode/iresharma')
      Future<Response> getUser(@QueryMap() Map<String, dynamic> queries);
    
  • @Headers: To send headers along, I am not sure but this is the only way you can send dynamic headers

      @Get(path: '/hashnode/iresharma')
      Future<Response> getUser(@Headers('search') String searchString);
    
  • @Body: To send a Body in case of POST or PUT or PATCH request

      @Post(path: '/hashnode/iresharma')
      Future<Response> getUser(@Body() Map<String, dynamic> queries);
    

Let's talk about the Request and Response objects

  • Response Object

    The response object is pretty simple and has only a few attributes

    • base

      which contains the Request, statuscode and some stuff

    • Body

      this contains body of the response. The datatype is determined by the converter you use

    • error

  • Request Object

    Well here's the variable declaration for the Request class

          final String method;
        final String baseUrl;
        final String url;
        final dynamic body;
        final List<PartValue> parts;
        final Map<String, dynamic> parameters;
        final Map<String, String> headers;
        final bool multipart;
    

    it's pretty self-explanatory

Interceptors

Interceptors are basically a set of commands that are run with every command

All interceptors are client wide

we put interceptors inside the client

static API create() {
    final client = ChopperClient(
        baseUrl: 'https://d2def3d9f930.ngrok.io',
        services: [_$API()],
                interceptors: [],
        converter: JsonConverter());
  }

A few built-in interceptors are:

  • HeadersInterceptor

      static API create() {
          final client = ChopperClient(
              baseUrl: 'https://d2def3d9f930.ngrok.io',
              services: [_$API()],
                      interceptors: [
                          HeadersInterceptor({'Content-type': 'Application/json'})
                      ],
              converter: JsonConverter());
        }
    

    The above code will add the 'Content-type': 'Application/JSON' header to all the requests that it makes.

  • HttpLoggingInterceptor

    We use this header to log everything that's happening with the request, but this requires some setup which is as follows:

    we include the logging package which is preloaded by chopper.

    Add make the following changes to main.dart

      void main() {
              Logger.root.level = Level.ALL;
              Logger.root.onRecord((rec) {
                  print('${rec.level) ${rec.message});        
              });
              runApp(MyAPP());
      }
    

    The above code will instantiate the Logger and now we can add the HttpLoggingInterceptor interceptor to the Client

      static API create() {
          final client = ChopperClient(
              baseUrl: 'https://d2def3d9f930.ngrok.io',
              services: [_$API()],
                      interceptors: [
                          HeadersInterceptor({'Content-type': 'Application/json'}),
                          HttpLoggingInterceptor()
                      ],
              converter: JsonConverter());
        }
    

    I personally use a custom logging interceptor, this prints way more information than needed and makes the debug console very very messy

  • CurlInterceptor

    This is not used as much but It's really cool because it prints out the curl command for the request just made

    Custom interceptors

    +โ€” async functions

    +โ€” class-based

    Async Functions

    we can create both request and response interceptors and I'd suggest not using the HttpLoggingInterceptor because it logs a lot of not so important information like the whole response which may make the more important information like the route, headers and query parameter difficult to read

      static API create() {
          final client = ChopperClient(
              baseUrl: 'https://d2def3d9f930.ngrok.io',
              services: [_$API()],
                      interceptors: [
                          HeadersInterceptor({'Content-type': 'Application/json'}),
                          // HttpLoggingInterceptor(),
                          (Request req) async {
                                  chopperLogger.info('${req.headers}');
                                  // VERY VERY IMPORTANT TO RETURN req
                                  return req;
                          }
                      ],
              converter: JsonConverter());
        }
    

    The above code is an example of request interceptor which logs the headers of the request, note it's very very important to return the req back from the function because the way it's made it does not give a lint but ends up crashing or compilation error

    below is an example of a response interceptor

      static API create() {
          final client = ChopperClient(
              baseUrl: 'https://d2def3d9f930.ngrok.io',
              services: [_$API()],
                      interceptors: [
                          HeadersInterceptor({'Content-type': 'Application/json'}),
                          // HttpLoggingInterceptor(),
                          (Request req) async {
                                  chopperLogger.info('${req.headers}');
                                  // VERY VERY IMPORTANT TO RETURN req
                                  return req;
                          },
                          (Response res) {
                              chopperLogger('${res.statusCode}');
                          }
                      ],
              converter: JsonConverter());
        }
    

    Custom Class-based interceptors

    Well according to our need we can have various reasons and functionality where we'd want to have interceptors but here we are gonna implement a simple interceptor which does not make the request if the user is on mobile data

    Important to note the below example uses connectivity package

      class MobileDataInterceptor implements RequestInterceptor {
          @override
          FutureOr<Request> onRequest(Request req) {
              final connection = await connectivity().checkConnnectivity();
              if(connection == ConnenctivityResult.mobile) {
                      // throw an appropriate exception
              }
              return req;
          }
      }
    

Custom convertors

The code below is a generic converter

import 'dart:convert';

import 'package:chopper/chopper.dart';

class GenericConvertor extends JsonConverter {
  final Map<Type, Function> typeToJsonFactoryMap;

  GenericConvertor(this.typeToJsonFactoryMap);

  @override
  Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
    return response.copyWith(
      body: fromJsonData<BodyType, InnerType>(
          response.body, typeToJsonFactoryMap[InnerType]),
    );
  }

  T fromJsonData<T, InnerType>(String jsonData, Function jsonParser) {
    var jsonMap = json.decode(jsonData);
    if (jsonMap is List) {
      return jsonMap
          .map((item) => jsonParser(item as Map<String, dynamic>) as InnerType)
          .toList() as T;
    }

    return jsonParser(jsonMap);
  }
}

How does this work ???

well the convertor is called like

GenericConvertor({
  UserModel: (jsonData) => UserModel.fromJson(jsonData),
  AuthCheck: (jsonData) => AuthCheck.fromJson(jsonData)
})

Basically, it takes in a Map<Type, function> corresponding to the class and the fromJson() function for each type

Then it runs a function which copies the response object and replaces the body with the object we want

it does that running the fromJson function and encoding the object as a bodyType, as required by the response object.

Even though it handles the list but that only works if the body of your response is directly a list else it breaks, and this true for normal values as well it works only if the body is directly the object you want

For a case where the data structure is different, you use the code snippet below as reference to write a custom converter

the data i received had body['chapters'] = List so i had to use this convertor

import 'dart:convert';

import 'package:chopper/chopper.dart';
import 'package:learners/models/DataModels/Chapters/chapterModel.dart';

class SubjectConvertor extends JsonConverter {
  @override
  Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
    return response.copyWith(
      body: fromJsonData<BodyType, InnerType>(response.body),
    );
  }

  T fromJsonData<T, InnerType>(String jsonData) {
    var jsonMap = json.decode(jsonData);
    List<dynamic> chapters = jsonMap['chapters'];
    if (chapters == null) {
      return ChapterModel.fromJson(jsonMap['chapter']) as T;
    }
    return chapters.map((e) => ChapterModel.fromJson(e)).toList() as T;
  }
}
for more watch the tutorial by reso coder

ResoCoder Chopper

Did you find this article valuable?

Support Iresh Sharma by becoming a sponsor. Any amount is appreciated!