12
Using the Chopper Library
Written by Kevin D Moore
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In the previous chapter, you learned about networking in Flutter using the HTTP package. Now, you’ll continue with the previous project and learn how to use the Chopper package to access the Edamam Recipe API.
Note: You can also start fresh by opening this chapter’s starter project. If you choose to do this, remember to click the Pub Get button or execute
flutter pub get
from Terminal. You’ll also need your API Key and ID.
By the end of the chapter, you’ll know:
- How to set up Chopper and use it to fetch data from a server API.
- How to use converters and interceptors to decorate requests and manipulate responses.
- How to log requests.
Why Chopper?
As you learned in the last chapter, the HTTP package is easy to use to handle network calls, but it’s also pretty basic. Chopper does a lot more. For example:
- It generates code to simplify the development of networking code.
- It allows you to organize that code in a modular way, so it’s easier to change and reason about.
Note: If you come from the Android side of mobile development, you’re probably familiar with the Retrofit library, which is similar. If you have an iOS background, AlamoFire is a very similar library.
Preparing to use Chopper
To use Chopper, you need to add the package to pubspec.yaml. To log network calls, you also need the logging package.
chopper: ^4.0.1
logging: ^1.0.1
chopper_generator: ^4.0.1
Handling recipe results
In this scenario, it’s a good practice to create a generic response class that will hold either a successful response or an error. While these classes aren’t required, they make it easier to deal with the responses that the server returns.
// 1
abstract class Result<T> {
}
// 2
class Success<T> extends Result<T> {
final T value;
Success(this.value);
}
// 3
class Error<T> extends Result<T> {
final Exception exception;
Error(this.exception);
}
Preparing the recipe service
Open recipe_service.dart. You need to have your API Key and ID for this next step.
// 1
import 'package:chopper/chopper.dart';
import 'recipe_model.dart';
import 'model_response.dart';
import 'model_converter.dart';
// 2
const String apiKey = '<Your Key Here>';
const String apiId = '<Your Id here>';
// 3
const String apiUrl = 'https://api.edamam.com';
// TODO: Add @ChopperApi() here
Setting up the Chopper client
Your next step is to create a class that defines your API calls and sets up the Chopper client to do the work for you. Still in recipe_service.dart, replace // TODO: Add @ChopperApi() here
with:
// 1
@ChopperApi()
// 2
abstract class RecipeService extends ChopperService {
// 3
@Get(path: 'search')
// 4
Future<Response<Result<APIRecipeQuery>>> queryRecipes(
// 5
@Query('q') String query, @Query('from') int from, @Query('to') int to);
// TODO: Add create()
}
// TODO: Add _addQuery()
Converting request and response
To use the returned API data, you need a converter to transform requests and responses. To attach a converter to a Chopper client, you need an interceptor. You can think of an interceptor as a function that runs every time you send a request or receive a response — a sort of hook to which you can attach functionalities, like converting or decorating data, before passing such data along.
import 'dart:convert';
import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'recipe_model.dart';
// 1
class ModelConverter implements Converter {
// 2
@override
Request convertRequest(Request request) {
// 3
final req = applyHeader(
request,
contentTypeKey,
jsonHeaders,
override: false,
);
// 4
return encodeJson(req);
}
Request encodeJson(Request request) {}
Response decodeJson<BodyType, InnerType>(Response response) {}
@override
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {}
}
Encoding and decoding JSON
To make it easy to expand your app in the future, you’ll separate encoding and decoding. This gives you flexibility if you need to use them separately later.
Encoding JSON
To encode the request in JSON format, replace the existing encodeJson()
with:
Request encodeJson(Request request) {
// 1
final contentType = request.headers[contentTypeKey];
// 2
if (contentType != null && contentType.contains(jsonHeaders)) {
// 3
return request.copyWith(body: json.encode(request.body));
}
return request;
}
Decoding JSON
Now, it’s time to add the functionality to decode JSON. A server response is usually a string, so you’ll have to parse the JSON string and transform it into the APIRecipeQuery
model class.
Response<BodyType> decodeJson<BodyType, InnerType>(Response response) {
final contentType = response.headers[contentTypeKey];
var body = response.body;
// 1
if (contentType != null && contentType.contains(jsonHeaders)) {
body = utf8.decode(response.bodyBytes);
}
try {
// 2
final mapData = json.decode(body);
// 3
if (mapData['status'] != null) {
return response.copyWith<BodyType>(
body: Error(Exception(mapData['status'])) as BodyType);
}
// 4
final recipeQuery = APIRecipeQuery.fromJson(mapData);
// 5
return response.copyWith<BodyType>(
body: Success(recipeQuery) as BodyType);
} catch (e) {
// 6
chopperLogger.warning(e);
return response.copyWith<BodyType>(
body: Error(e as Exception) as BodyType);
}
}
@override
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
// 1
return decodeJson<BodyType, InnerType>(response);
}
Using interceptors
As mentioned earlier, interceptors can intercept either the request, the response or both. In a request interceptor, you can add headers or handle authentication. In a response interceptor, you can manipulate a response and transform it into another type, as you’ll see shortly. You’ll start with decorating the request.
Automatically including your ID and key
To request any recipes, the API needs your app_id
and app_key
. Instead of adding these fields manually to each query, you can use an interceptor to add them to each call.
Request _addQuery(Request req) {
// 1
final params = Map<String, dynamic>.from(req.parameters);
// 2
params['app_id'] = apiId;
params['app_key'] = apiKey;
// 3
return req.copyWith(parameters: params);
}
Wiring up interceptors & converters
It’s time to create an instance of the service that will fetch recipes.
static RecipeService create() {
// 1
final client = ChopperClient(
// 2
baseUrl: apiUrl,
// 3
interceptors: [_addQuery, HttpLoggingInterceptor()],
// 4
converter: ModelConverter(),
// 5
errorConverter: const JsonConverter(),
// 6
services: [
_$RecipeService(),
],
);
// 7
return _$RecipeService(client);
}
Generating the Chopper file
Your next step is to generate recipe_service.chopper.dart, which works with the part
keyword. Remember from Chapter 10, “Serialization With JSON”, part
will include the specified file and make it part of one big file.
part 'recipe_service.chopper.dart';
flutter pub run build_runner build --delete-conflicting-outputs
Logging requests & responses
Open main.dart and add the following import:
import 'package:logging/logging.dart';
void _setupLogging() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((rec) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
});
}
_setupLogging();
Using the Chopper client
Open ui/recipes/recipe_list.dart. You’ll see some errors due to the changes you’ve made.
import 'dart:convert';
import 'package:chopper/chopper.dart';
import '../../network/model_response.dart';
import 'dart:collection';
return FutureBuilder<APIRecipeQuery>(
return FutureBuilder<Response<Result<APIRecipeQuery>>>(
future: RecipeService.create().queryRecipes(
searchTextController.text.trim(),
currentStartPosition,
currentEndPosition),
final query = snapshot.data;
// 1
if (false == snapshot.data?.isSuccessful) {
var errorMessage = 'Problems getting data';
// 2
if (snapshot.data?.error != null &&
snapshot.data?.error is LinkedHashMap) {
final map = snapshot.data?.error as LinkedHashMap;
errorMessage = map['message'];
}
return Center(
child: Text(
errorMessage,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18.0),
),
);
}
// 3
final result = snapshot.data?.body;
if (result == null || result is Error) {
// Hit an error
inErrorState = true;
return _buildRecipeList(context, currentSearchList);
}
// 4
final query = (result as Success).value;
Key points
- The Chopper package provides easy ways to retrieve data from the internet.
- You can add headers to each network request.
- Interceptors can intercept both requests and responses and change those values.
- Converters can modify requests and responses.
- It’s easy to set up global logging.
Where to go from here?
If you want to learn more about the Chopper package, go to https://pub.dev/packages/chopper. For more info on the Logging library, visit https://pub.dev/packages/logging.