Chapters

Hide chapters

Flutter Apprentice

Third Edition · Flutter 3.3 · Dart 2.18.0 · Android Studio 2021.2.1

Section IV: Networking, Persistence and State

Section 4: 7 chapters
Show chapters Hide chapters

Appendices

Section 7: 2 chapters
Show chapters Hide chapters

12. Using a Network 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 scrambled text.

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.6
logging: ^1.0.2
chopper_generator: ^4.0.6

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 'dart:developer';
import 'package:logging/logging.dart';
void _setupLogging() {
  Logger.root.level = Level.ALL;
  Logger.root.onRecord.listen(
    (rec) {
      log('${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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now