This article by Jian Yue SimpRead transcoding, original address www.raywenderlich.com

This chapter will teach you how to retrieve data over the Internet using Chopper packages. Unlike the networking methods built into Dart, this package easily does all the necessary tasks required for REST API communication.

In the previous chapter, you learned about using HTTP packets to connect to the Internet in Flutter. Now, you will continue with the previous project and learn how to access the Edam Recipe API using Chopper packages.

Note: You can also start over by opening the Starter project in this chapter. If you choose to do so, remember to click the Get Dependencies button or perform flutter pub Get from your terminal. You also need to add your API key and ID.

By the end of this chapter, you will know.

  • How to set up Chopper and use it to get data from the server API.
  • How to use converters and interceptors to decorate request and action responses.
  • How to log requests.

Why Chopper?

As you learned in the previous chapter, the HTTP package is easy to use to handle network calls, but it is also fairly basic. Chopper can do so much more. For example.

  • It generates code to simplify network code development.
  • It allows you to organize this code in a modular way, so it’s easier to change and reason about.

Note: If you’re 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.

Get ready to use Chopper

To use Chopper, you need to add the package to pubspec.yaml. To log network calls, you also need a logging package.

Open pubspec.yaml and replace the HTTP package line with.

chopper: ^ 3.0.6
logging: ^ 0.11.4


Copy the code

You also need chopper_generator, which is a package in the form of a part file that generates template code for you. Add this in the dev_DEPENDENCIES section, after jSON_serialIZABLE.

chopper_generator: ^ 3.0.6


Copy the code

Next, click on Pub get or run ‘FLUTTER Pub get’ on your terminal to get the new package.

The new package is now ready to use…… Fasten your seat belt! :]

Treatment formula results

In this case, it is good practice to create a generic response class that will hold either a successful response or an error. Although these classes are not required, they make it easier to process the response returned by the server.

Right click lib/network to create a new DART file called model_Response.dart. Add the following classes to it.

/ / 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);
}


Copy the code

Here you have:

  1. Created aabstract class. It is a generic typeTRequest result template.
  2. To create theSuccessClass to extendResultAnd saves a value if the response succeeds. For example, this can save JSON data.
  3. To create theErrorClass to extendResultSave an exception. This simulates errors that occur during HTTP calls, such as when you use the wrong credentials or try to retrieve data without authorization.

Note: to review the Dart in the knowledge of the abstract class, please check our Dart apprentice book www.raywenderlich.com/books/dart-… .

You will use these classes to model the data obtained over HTTP using Chopper.

Preparation service

Open recipe_service.dart and delete the existing RecipeService class and getData method. Replace the HTTP package import with the following.

import 'package:chopper/chopper.dart';
import 'recipe_model.dart';
import 'model_response.dart';

Copy the code

This adds Chopper bags and your model.

Replace the existing apiUrl constant with.

const String apiUrl = 'https://api.edamam.com';

Copy the code

This lets you call other apis besides /search.

Now it’s time to set up the Chopper!

Set up the Chopper client

Your next step is to create a class that defines your API calls and set up the Chopper client to do the work for you. Again, in recipe_service.dart, add the following.

/ / 1
@ChopperApi(a)/ / 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);
}


Copy the code

There’s quite a bit to understand here. I want to break it down.

  1. @ChopperApi()Tell the Chopper generator to build onepartFile. This generated file will have the same name as this file, but add on top of itchopper. In this case, it will berecipe_service.chopper.dart. Such a file would hold the template code.
  2. “RecipeService “is an” abstract “class because you just define the signature of the method. The generator script will take these definitions and generate all the required code.
  3. @GetIs an annotation that tells the generator that this is aGETRequest, has a name calledsearchthepathYou were fromapiUrlThe path was deleted from the. You can also use other HTTP methods such as@Post,@Putand@DeleteBut you will not use them in this chapter.
  4. You define a function, using the one you created earlierAPIRecipeQueryReturns aResponsetheFuture. The abstract that you create on topResultWill hold either a value or an error.
  5. queryRecipes()The use of Chopper@QueryNote to accept onequeryString andfromandtoAn integer. This method does not have a body. The generator script will create the body of the function with all the parameters.

Notice that so far you have defined a generic interface to make network calls. There is no actual code to perform tasks such as adding API keys to requests or converting responses into data objects. That’s the job of the converters and interceptors!

Transform the request and response

In order to use the returned API data, you need a converter to convert the request and response. In order to attach the converter to the 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 hook that you can attach some functionality to, such as converting or decorating data, before passing it along.

Right click on lib/network, create a new file called model_Converter.dart and add the following.

import 'dart:convert';
import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'recipe_model.dart';

Copy the code

This adds the built-in Dart Convert package, which converts data to JSON, plus the Chopper package and your model files.

Next, create the ModelConverter by adding.

/ / 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) {}
}


Copy the code

Here’s what you do with this code.

  1. useModelConverterTo implement the ChopperConverterAn abstract class.
  2. coverconvertRequest()It receives a request and returns a new one.
  3. usejsonHeadersAdd a header to the request stating that your request type isapplication/json. These constants are part of Chopper.
  4. callencodeJson()Convert the request into a JSON-encoded request, which is required by the server API.

The rest of the code consists of placeholders, which you will add in the next section.

Encode and decode JSON

In order to expand your application in the future, you will separate the encoding and decoding. This gives you flexibility if you need to use them separately later.

Whenever you make a network call, make sure you encode the request before sending it and decode the response string into your model class, which you will use to display data in the user interface.

JSON encoding

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;
}


Copy the code

In this code, you.

  1. Extract the request header.
  2. confirmcontentTypeisapplication/jsonType.
  3. Make a copy of the request with JSON encoding.

Basically, this method takes a Request instance and returns a decorated copy ready to be sent to the server. Decoding? I’m glad you asked. ]

Decoding JSON

Now, it’s time to add the ability to decode JSON. The response from the server is usually a string, so you must parse the JSON string and convert it to the APIRecipeQuery model class.

Replace decodeJson() with.

Response 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) asBodyType); }}Copy the code

There’s a lot to think about here. To break it down, you.

  1. Check to see if you are working with JSON and decode response into a string named body.

  2. Convert the string to a map representation using JSON decoding.

  3. When an error occurs, the server returns a field named Status. Here, you check whether the map contains such a field. If so, you return a response with an embedded instance of Error.

  4. Convert the map to a model class using apirecipeQuery.fromjson ().

  5. Return a successful response wrapped around the recipeQuery.

  6. If you get any other type of Error, wrap the response with a generic instance of ‘Error’.

You still need to override one method: convertResponse. This method changes the given response to what you want.

Replace the existing convertResponse() with.

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


Copy the code
  1. This is just calling what you defined beforedecodeJson.

Now it’s time to use converters and add some interceptors where appropriate.

Use interceptors

As mentioned earlier, interceptors can intercept requests, responses, or both. In the request interceptor, you can add headers or handle authentication. In the response interceptor, you can manipulate the response and convert it to another type, as you’ll see shortly. You will start with the decorator request.

Automatically include your ID and key

In order to request any recipe, the API needs your app_id and app_key. You can use an interceptor to add these fields to each call instead of manually adding them to each query.

Open recipe_service.dart and add the following after the “RecipeService “class declaration.

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);
}


Copy the code

This is a request interceptor that adds the API key and ID to the query parameters. Here’s what the code does.

  1. To create aMapWhich contains information from existingRequestThe key-value pair of the parameter.
  2. willapp_idandapp_keyAdd parameters to the map.
  3. Return a new oneRequestA copy that contains the parameters in the map.

The nice thing about this method is that once you have it up, all your calls will use it. Although you only have one call now, if you add more calls, they will automatically include these keys. If you want to add a new parameter to each call, you can simply change the method. Are you starting to see the advantages of Chopper? ]

You have interceptors to decorate requests, and you have a converter to convert responses into model classes. Next, you need to put them to use!

Interceptor and converter connection

Add the following import statement at the top of reciPE_service.dart.

import 'model_converter.dart';


Copy the code

Now add this new method to the RecipeService. Make sure you don’t add it to _addQuery(). Don’t worry about those red slashes, which warn you that you’re missing template code because you haven’t generated it yet.

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);
}


Copy the code

In this code, you

  1. To create aChopperClientInstance.
  2. Pass a base URL using the ‘apiUrl’ constant.
  3. Pass in two interceptors._addQuery()Add your key and ID to the query.HttpLoggingInterceptorIt’s part of Chopper. Logs all calls. It’s easy to see the traffic between the application and the server when you’re developing.
  4. willconverterSet toModelConverterAn instance of.
  5. Use the built-inJsonConverterTo decode any errors.
  6. Defines the service that is created when you run the generator script.
  7. Returns an instance of the generated service.

You’re all set and ready to generate template code!

Generate Chopper file

Your next step is to generate recipe_service.chopper.dart, which works with the part. Remember from Chapter 10, “Serializing with JSON,” that part will include the specified file and make it part of a larger file.

Import the file you want to generate. Open reciPE_service.dart and append this to the import statement.

part 'recipe_service.chopper.dart';


Copy the code

Ignore the red squares. They disappear after you generate the file.

Note: Importing a file before it is created may seem strange, but if the generator script doesn’t know where the declared comment is, it will fail.

Now, open Terminal in Android Studio. By default, it will be in your project folder. The execution.

flutter pub run build_runner build --delete-conflicting-outputs


Copy the code

Note: Using — delete-Conflicting – Outputs will delete all generated files before generating new ones.

During execution, you’ll see something like this.

Once it’s done, you’ll see the new recipe_service.chopper.dart in lib/network. You may need to refresh the Network folder before it appears.

Note: If you do not see this file or Android Studio does not detect its presence, please restart Android Studio.

Open it and check. The first thing you’ll see is a comment telling you not to modify the file manually.

Further down, you’ll see a class called _$RecipeService. Below this, you’ll notice that queryRecipes() has been rewritten to set up parameters and requests. It uses the client to send requests.

This may not seem like much, but when you add different calls to different paths and parameters, you’ll start to appreciate help like the code generator included in Chopper :]

Now that you’ve changed the RecipeService to use Chopper, it’s time to put some finishing touches on it. Set up logging and use new methods to get data.

Log requests and responses

Open in.dart and add the following imports.

import 'package:logging/logging.dart';


Copy the code

This is from the logging package you added to pubspec.yaml earlier.

After “main() “, add.

void _setupLogging() {
  Logger.root.level = Level.ALL;
  Logger.root.onRecord.listen((rec) {
    print('${rec.level.name}: ${rec.time}: ${rec.message}');
  });
}


Copy the code

This initializes the logging package and allows Chopper to log requests and responses. Set the Level to level. ALL so that you can see each logging statement.

Note: You can try changing ALL to WARNING, SEVERE, or another level to see what happens.

In main(), add the following as the first statement.

_setupLogging();


Copy the code

The records are now set up. Now it’s time to use the new Chopper based feature :]

Use the Chopper client

Open the UI/recipes/recipe_list. Dart. You will see some errors due to the changes you have made.

If you see the import below, delete it because it has already been imported in another class.

import 'dart:convert';


Copy the code

Now add the following imports.

import 'package:chopper/chopper.dart';
import '.. /.. /network/model_response.dart';


Copy the code

Find the declaration for getRecipeData() and delete it.

In _buildRecipeLoader(), replace the following line // TODO: change to the new response, from.

return FutureBuilder<APIRecipeQuery>(


Copy the code

To:

return FutureBuilder<Response<Result<APIRecipeQuery>>>(


Copy the code

This uses a new response type that wraps the result of the API call.

Now replace the following future with // TODO: change with new RecipeService.

future: RecipeService.create().queryRecipes(
    searchTextController.text.trim(),
    currentStartPosition,
    currentEndPosition),


Copy the code

Future now creates a new instance of RecipeService and calls its method queryRecipes() to execute the query.

Finally, replace the following line // TODO: Change with the new snapshot from:

final query = snapshot.data;


Copy the code

To:

/ / 1
final result = snapshot.data.body;
/ / 2
if (result is Error) {
  // Hit an error
  inErrorState = true;
  return _buildRecipeList(context, currentSearchList);
}
/ / 3
final query = (result as Success).value;


Copy the code

Here’s what you did in the code above.

  1. snapshot.dataNow it’s aResponseInstead of being a string.bodyThe field is what you defined abovesuccessfulorerror. willbodyTo extract the value ofresultIn the.
  2. ifresultIs an error that returns the current recipe list.
  3. Due to theresultPassed error checking and treated it asSuccessAnd extract its value toqueryIn the.

Run the application and enter a search value, such as chicken. Click the search icon to verify that you see the recipe displayed in the user interface.

Now, look in Android Studio’s Run window and you’ll see a lot of INFO about your network calls. This is a great way to look at your requests and responses and figure out what’s causing any problems.

You did it! You can now use Chopper to call the server’s API and retrieve the recipe.

The key point

The Chopper package provides an easy way to retrieve data from the Internet.

  • You can add headers to each network request.
  • Interceptors can intercept requests and responses and change these values.
  • Converters can modify requests and responses.
  • Global logging is easy to set up.

Where to go?

If you want to learn more about Chopper packs go to pub.dev/packages/ch… . For more information about log libraries, visit pub.dev/packages/lo… .

In the next chapter, you will learn about the important topic of state management. Until then!


www.deepl.com translation