Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 chapters
Show chapters Hide chapters

12. Networking in Flutter
Written by Kevin D Moore

Loading data from the network to show it in a UI is a very common task for apps. In the previous chapter, you learned how to serialize JSON data. Now, you’ll continue the project to learn about retrieving JSON data from the network.

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.

By the end of this chapter, you’ll know how to:

  • Sign up for a recipe API service.
  • Trigger a search for recipes by name.
  • Convert data returned by the API to model classes.
  • Display recipes in the current UI.

Without further ado, it’s time to get started!

Signing Up With the Recipe API

For your remote content, you’ll use the Spoonacular Food API. Open this link in your browser: https://spoonacular.com/food-api.

Click the Start Now button in the top right to create an account.

Fill in an email and password, then click the checkbox and sign up. Go through the steps to finish the process. You can choose the free tier.

Click Sign Up, and you should see this:

Once you’ve confirmed your email, visit https://spoonacular.com/food-api/console and log in.

You should see your Console. Once you start making requests, you’ll see the graph fill up.

Now go to docs and click Full Documentation:

Here, you can see the docs for searching for recipes:

If you scroll down, you can see a lot of fields returned. We’re not interested in most of these fields.

You’ll see a complete API URL and a list of the parameters available for the GET request you’ll make.

There’s much more API information on this page than you’ll need for your app, so you might want to bookmark it for the future.

Click My Console, then the Profile section and you’ll end up on this link https://spoonacular.com/food-api/console#Profile:

Click Show/Hide API key. Copy the API Key and save it in a secure place.

For your next step, you’ll use your newly created API key to fetch recipes via HTTP requests.

Note: The free developer version of the API is rate-limited. If you use the API a lot, you’ll probably receive some JSON responses with errors and emails warning you about the limit.

Preparing the Pubspec File

Open either your project or the chapter’s starter project. To use the http package for this app, you need to add it to pubspec.yaml, so open that file and add the following after the json_annotation package:

http: ^1.1.0

Click the Pub get button to install the package, or run flutter pub get from the Terminal.

Using the HTTP package

The package contains only a few files and methods that you’ll use in this chapter. The REST protocol has methods such as:

  • GET: Gets the data.
  • POST: Posts/sends new data.
  • PUT: Updates data.
  • DELETE: Deletes data.

You’ll use GET, specifically the function get() in the http package, to retrieve recipe data from the API. This function uses the API’s URL and a list of optional headers to retrieve data from the API service. In this case, you’ll send all the information via query parameters.

Connecting to the Recipe Service

To fetch data from the recipe API, you’ll create a Dart class to manage the connection. Such a class file will contain your API Key and URL.

In the Project sidebar, right-click lib/network, create a new Dart file and name it spoonacular_service.dart. After the file opens, import the HTTP package along with the required files:

import 'dart:convert';
import 'dart:developer';
import 'package:http/http.dart' as http;

import '../data/models/recipe.dart';
import '../mock_service/mock_service.dart';
import 'model_response.dart';
import 'query_result.dart';
import 'service_interface.dart';
import 'spoonacular_model.dart';

Note: Here, we import the http package as http so that we can append http to the get() method and prevent any naming conflicts or confusion.

Now, add the constants that you’ll use when calling the APIs:

const String apiKey = '<Add Your Key Here>';
const String apiUrl = 'https://api.spoonacular.com/';

Copy the API key from your Spoonacular account and replace the existing apiKey string with your value.

The apiUrl constant holds the base URL for the Spoonacular search API from the recipe API documentation. You’ll append the path to this URL to get the data you want.

Still in spoonacular_service.dart, add the following class and method to get the data from the API:

class SpoonacularService implements ServiceInterface {
   // 1
  Future getData(String url) async {
    // 2
    final response = await http.get(Uri.parse(url));
    // 3
    if (response.statusCode == 200) {
      // 4
      return response.body;
    } else {
      // 5
      log(response.statusCode.toString());
    }
  }
  // TODO: Add getRecipes
}

Here’s a breakdown of what’s going on:

  1. getData() returns a value in Future, with an upper case “F”, because it takes some time to get the data from the Server. An API’s returned data type is determined in the future, lower case “f”. async signifies this method performs an asynchronous operation.

  2. response has to wait until the HTTP gets the data from the server. The await keyword tells the function to wait. Response and get() are from the HTTP package. get() fetches data from the provided url.

  3. A statusCode of 200 means the request was successful.

  4. You return the results embedded in response.body.

  5. Otherwise, print the statusCode to the console if you have an error.

Note: To learn more about Future and async operations, check out chapters 11 and 12 of Dart Apprentice: Beyond the Basics book https://www.kodeco.com/books/dart-apprentice-beyond-the-basics.

Now, replace // TODO: Add getRecipes with:

// 1
@override
Future<Result<Recipe>> queryRecipe(String recipeId) {
  // TODO: implement queryRecipe
  throw UnimplementedError();
}

// 2
@override
Future<RecipeResponse> queryRecipes(
    String query, int offset, int number) async {
  // 3
  final recipeData = await getData(
      '${apiUrl}recipes/complexSearch?apiKey=$apiKey&query=$query&offset=$offset&number=$number');
  // 4
  final spoonacularResults =
      SpoonacularResults.fromJson(jsonDecode(recipeData));
  // 5
  final recipes = spoonacularResultsToRecipe(spoonacularResults);
  // 6
  final apiQueryResults = QueryResult(
      offset: spoonacularResults.offset,
      number: spoonacularResults.number,
      totalResults: spoonacularResults.totalResults,
      recipes: recipes);
  // 7
  return Success(apiQueryResults);
}

In this code, you:

  1. Override an unimplemented method for querying a specific recipe. That will be implemented later.
  2. Create a new method, queryRecipes(), with the parameters query, offset and number. These help you get specific pages from the complete query. offset starts at 0, and number is calculated by adding the offset index to your page size. You use a return type of Future<RecipeResponse> for this method because the response will be a RecipeResponse in the future when it finishes. async signals that this method runs asynchronously.
  3. final creates a non-changing variable. You use await to tell the app to wait until getData() returns its result. Look closely at getData() and note that you’re creating the API URL with the variables passed in.
  4. Convert the JSON string to a SpoonacularResults class with the help of fromJson method.
  5. Convert the SpoonacularResults class object into a list of recipes.
  6. Create a QueryResult object with those results.
  7. Return a Success with the query results.

Note: This method doesn’t handle errors.

Now that you’ve written the service, it’s time to update the UI code to use it.

Updating the User Interface

Open main.dart and add the following:

import 'network/spoonacular_service.dart';

Then, after the sharedPrefProvider override, replace:

final service = await MockService.create();

with:

final service = SpoonacularService();

This creates a new instance of SpoonacularService. This will be the start of using real data taken from the internet instead of mock data.

Now remove the import of mock_service.dart, as it’s not needed anymore.

Retrieving Recipe Data

Great, it’s time to try out the app!

Run the app, type Chicken in the text field, and tap the Search icon. While the app gets data from the API, you’ll see the circular progress bar.

After the app receives the data, you’ll see a list of images with different types of chicken recipes.

Well done! You’ve updated your app to receive real data from the internet. Try different search queries and go and show your friends what you’ve created. :]

Note: If you make too many queries, you could get an error from the Spoonacular site. That’s because the free account limits your number of calls.

The http package is easy to use to handle network calls, but it’s also pretty basic. Let’s explore Chopper, a library that simplifies the creation of code that manages HTTP calls.

Why Chopper?

Chopper is a library that streamlines the process of writing code that performs HTTP requests. For example:

  • It generates code to simplify the development of networking code.
  • It allows you to organize that code modularly, making it easier to change.

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, which is already included in the project.

Open pubspec.yaml and add the following after the HTTP package:

chopper: ^6.1.4

You also need chopper_generator, which is a package that generates the boilerplate code for you in the form of a part file. In the dev_dependencies section, after json_serializable, add the following:

chopper_generator: ^6.0.3

Next, either click Pub get or run flutter pub get in Terminal to get the new packages.

Now that the new packages are ready to be used… fasten your seat belt! :]

Handling Recipe Results

In this scenario, creating a generic response class that holds either a successful response or an error is good practice. While these classes aren’t required, they make it easier to deal with the responses that the server returns.

Take a look inside the lib/network folder and open model_response.dart.

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

Here’s what that does:

  1. Defines a sealed class. It’s a simple blueprint for a result with a generic type T.
  2. The Success class extends Result and holds a value when the response is successful. This could hold JSON data or a de-serialized class.
  3. The Error class extends Result and holds an exception. This will model errors that occur during an HTTP call, like using the wrong credentials or trying to fetch data without authorization.

Note: The sealed modifier prevents a class from being extended or implemented outside its own library. Sealed classes are implicitly abstract. To refresh your knowledge of classes in Dart, check out our Dart Apprentice: Fundamentals book https://www.kodeco.com/books/dart-apprentice-fundamentals/.

You’ll use these classes to model the data fetched via HTTP using Chopper. Now that Chopper has been added, you need to update the definition of the result types defined in lib/network/service_interface.dart. In that file, add the Chooper import:

import 'package:chopper/chopper.dart';

Then replace:

typedef RecipeResponse = Result<QueryResult>;
typedef RecipeDetailsResponse = Result<Recipe>;

with:

typedef RecipeResponse = Response<Result<QueryResult>>;
typedef RecipeDetailsResponse = Response<Result<Recipe>>;

Instead of returning a Result directly, you’ll return a Response that contains a Result. This is because Chopper will handle the conversion of the response to a Result for you.

This will mess up the existing MockService class as now you have passed Response instead of QueryResult. Open up mock_service.dart and add the following imports:

import 'package:http/http.dart' as http;
import 'package:chopper/chopper.dart';

In the queryRecipes methods, wrap each Success call with a Response. Like this:

return Future.value(
          Response(
            http.Response(
              'Dummy',
              200,
              request: null,
            ),
            Success<QueryResult>(_currentRecipes1),
          ),
        );

Do this three times. Make sure you keep the correct values.

Also, modify queryRecipe() like this:

  return Future.value(
      Response(
        http.Response(
          'Dummy',
          200,
          request: null,
        ),
        Success<Recipe>(recipeDetails),
      ),
    );

Now it’s time to integrate the code that Chopper will generate into the existing service.

Preparing the Recipe Service

Open spoonacular_service.dart.

Replace the existing imports with the following:

import 'package:chopper/chopper.dart';

import 'model_response.dart';
import 'query_result.dart';
import 'service_interface.dart';
import '../data/models/models.dart';

part 'spoonacular_service.chopper.dart';

The .chopper file doesn’t exist yet, but you’ll generate it soon. Change the definition of the class to look like this:

// 1
@ChopperApi()
// 2
abstract class SpoonacularService extends ChopperService
    implements ServiceInterface {
  1. @ChopperApi() tells the Chopper generator to build a file. This generated file will have the same name as this file but with .chopper added to it. In this case, it will be spoonacular_service.chopper.dart. Such a file will hold the boilerplate code.
  2. Define an abstract class. Chopper will create the real class that extends the ChopperService and implements the ServiceInterface.

Now remove the getData() method. It’s now time to set up Chopper!

Setting Up the Chopper Client

Your next step is to update the queries needed to implement the service. Replace the definitions of queryRecipes() and queryRecipe() with:

/// Get the details of a specific recipe
@override
@Get(path: 'recipes/{id}/information?includeNutrition=false')
Future<RecipeDetailsResponse> queryRecipe(
  @Path('id') String id,
);

/// Get a list of recipes that match the query string
@override
@Get(path: 'recipes/complexSearch')
Future<RecipeResponse> queryRecipes(
  @Query('query') String query,
  @Query('offset') int offset,
  @Query('number') int number,
);

// TODO: Add create Service

The first method returns the details of a specific recipe. The second method returns a list of recipes:

  • @Get is an annotation that tells the generator this is a GET request.
  • path is the path to the API call. Chopper will append this path to the base URL, which you’ve defined as the apiUrl constant in SpoonacularService class.
  • In the first method, you’re using a path parameter to get the details of the specific recipe by passing a recipe ID as a dynamic parameter. In the second method, you’re using a path to get a list of recipes.
  • There are other HTTP methods you can use, such as @Post, @Put and @Delete, but you won’t use them in this chapter.
  • @Query is a query parameter used to define the query name in the URL that’s created for this API call. In the second method, you’re using @Query to get the query, offset and number of recipes.
  • These methods return a Future response.

Note that you have defined a generic interface to make network calls so far. No actual code performs tasks like adding the API key to the request or transforming the response into data objects. This is a job for converters and interceptors.

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. It’s a sort of hook to which you can attach functionalities, like converting or decorating data, before passing such data along.

Right-click lib/network, create a new file named spoonacular_converter.dart and add the following imports:

import 'dart:convert';
import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'query_result.dart';
import 'spoonacular_model.dart';

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

Next, create SpoonacularConverter by adding the following:

// 1
class SpoonacularConverter implements Converter {
  // 2
  @override
  Request convertRequest(Request request) {
    // 3
    final req = applyHeader(
      request,
      contentTypeKey,
      jsonHeaders,
      override: false,
    );

    // 4
    return encodeJson(req);
  }

  // TODO encode JSON

  // TODO Decode Json

  // TODO Convert Response to Model
}

Here’s what you’re doing with this code:

  1. Create SpoonacularConverter class to implement the Chopper Converter abstract class.
  2. Override convertRequest(), which takes in a request and returns a new request.
  3. Add a header to the request that says you have a request type of application/json using jsonHeaders. These constants are part of Chopper.
  4. Call encodeJson() to convert the request to a JSON-encoded one, as required by the server API.

The remaining code consists of placeholders, which you’ll include in the next section.

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.

Whenever you make network calls, you want to ensure that you encode the request before you send it and decode the response string into your model classes, which you’ll use to display data in the UI.

Encoding JSON

To encode the request in JSON format, replace // TODO encode JSON with the following:

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

In this code, you:

  1. Get the content type from the request headers.
  2. Check if contentType is not null and contentType is of type application/json.
  3. Return a copy of the request with a JSON-encoded body.

Essentially, this method takes a Request instance and returns an encoded copy ready to be sent to the server. What about decoding? Well, I’m glad you asked. :]

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 corresponding model class.

Replace // TODO Decode Json with:

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) as Map<String, dynamic>;

      // 3
      // This is the list of recipes
      if (mapData.keys.contains('totalResults')) {
        // 4
        final spoonacularResults = SpoonacularResults.fromJson(mapData);
        // 5
        final recipes = spoonacularResultsToRecipe(spoonacularResults);
        // 6
        final apiQueryResults = QueryResult(
            offset: spoonacularResults.offset,
            number: spoonacularResults.number,
            totalResults: spoonacularResults.totalResults,
            recipes: recipes);
        // 7
        return response.copyWith<BodyType>(
          body: Success(apiQueryResults) as BodyType,
        );
      } else {
        // This is the recipe details
        // 8
        final spoonacularRecipe = SpoonacularRecipe.fromJson(mapData);
        // 9
        final recipe = spoonacularRecipeToRecipe(spoonacularRecipe);
        // 10
        return response.copyWith<BodyType>(
          body: Success(recipe) as BodyType,
        );
      }
    } catch (e) {
      // 11
      chopperLogger.warning(e);
      final error = Error<InnerType>(Exception(e.toString()));
      return Response(response.base, null,
          error: error);
    }
}

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

  1. Check if the contentType is not null and check if contentType contains the jsonHeaders. Later you decode the response and save to body.
  2. Use JSON decoding to convert that string into a map representation.
  3. Check if the call has the “totalResults” text. This means it’s from the queryRecipes call.
  4. Convert the JSON to a SpoonacularResults instance using fromJson().
  5. Convert SpoonacularResults to a list of recipes.
  6. Create a QueryResult with the recipes.
  7. Return a copy of Response with Success result.
  8. Convert the map to a detailed SpoonacularRecipe.
  9. Convert the spoonacularRecipe to the recipe.
  10. Return a copy of Response with Success that wraps the result.
  11. If you get any kind of error, wrap the response with a generic instance of Error.

You still have to override one more method: convertResponse(). This method changes the given response to the one you want.

Replace the existing // TODO Convert Response to Model with the following:

@override
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
  // 1
  return decodeJson<BodyType, InnerType>(response);
}
  1. This returns the decoded JSON response by calling decodeJson(), which you defined earlier.

Now, it’s time to use the converter in the appropriate spots and to add some interceptors.

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 API Key

To request any recipes, the API needs your api_key. Instead of adding this field manually to each query, you can use an interceptor to add this to each call.

Open spoonacular_service.dart and add the following method outside of the SpoonacularService class definition:

Request _addQuery(Request req) {
  // 1
  final params = Map<String, dynamic>.from(req.parameters);
  // 2
  params['apiKey'] = apiKey;
  // 3
  return req.copyWith(parameters: params);
}

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

  1. Creates a Map, which contains key-value pairs from the existing Request parameters.
  2. Adds the apiKey parameter to the map.
  3. Returns a new copy of the Request with the parameters contained in the map.

The benefit of this method is that once you hook it up, all your calls will use it. While you only have one call for now, if you add more, they’ll include those parameters automatically. Also, if you want to add a new parameter to every call, you’ll change only this method.

I hope you’re starting to see the advantages of Chopper. :]

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

Wiring Up Interceptors and Converters

It’s time to create an instance of the service that will fetch recipes.

Still in spoonacular_service.dart, add the following import, and make sure it’s placed before the part statement:

import 'spoonacular_converter.dart';

Then locate // TODO: Add create Service and replace it with the following code. Don’t worry about the red squiggles; they’re warning you that the boilerplate code is missing because you haven’t generated it yet.

static SpoonacularService create() {
  // 1
 final client = ChopperClient(
    // 2
    baseUrl: Uri.parse(apiUrl),
    // 3
    interceptors: [_addQuery, HttpLoggingInterceptor()],
    // 4
    converter: SpoonacularConverter(),
    // 5
    errorConverter: const JsonConverter(),
    // 6
    services: [
      _$SpoonacularService(),
    ],
  );
  // 7
  return _$SpoonacularService(client);
}

In this code, you:

  1. Create a ChopperClient instance.
  2. Pass in a base URL using the apiUrl constant.
  3. Pass in two interceptors. _addQuery() adds your API key to the query. HttpLoggingInterceptor is part of Chopper and logs all calls. While you’re developing, it’s handy to see traffic between the app and the server.
  4. Set the converter as an instance of SpoonacularConverter.
  5. Use the built-in JsonConverter to decode any errors.
  6. Define the services created when you run the generator script.
  7. Return an instance of the generated service.

It’s all set, you’re ready to generate the boilerplate code!

Generating the Chopper File

Your next step is to generate spoonacular_service.chopper.dart, which works with the part keyword. Remember from Chapter 11, “Serialization With JSON”, part will include the specified file and make it part of one big file.

Note: It might seem weird to import a file before it’s been created, but the generator script will fail if it doesn’t know what file to create.

Now, open Terminal in Android Studio. By default, it’ll be in your project folder.

Execute the following:

dart run build_runner build --delete-conflicting-outputs

Note: Using --delete-conflicting-outputs will delete all generated files before generating new ones.

While it’s executing, you’ll see something like this:

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

Note: In case you don’t see the file or Android Studio doesn’t detect its presence, right-click on the network folder and select “Reload from disk”.

Open it and check it out. The first thing you’ll see is a comment stating not to modify the file by hand.

Looking farther down, you’ll see a class called _$SpoonacularService. Below that, you’ll notice that queryRecipes() has been overridden to build the parameters and the request. It uses the client to send the request.

It may not seem like much, but as you add different calls with different paths and parameters, you’ll start to appreciate the help of a code generator like the one included in Chopper.

Now that you’ve changed SpoonacularService to use Chopper, it’s time to put on the finishing touches.

Using the Chopper Client

Open main.dart and after sharedPrefs, replace:

  final service = SpoonacularService();

with:

final service = SpoonacularService.create();

This method will create a new instance of the service with the Chopper client.

Updating the UI

Now open lib/ui/recipe_details.dart. In loadRecipe(), replace:

final result = response;
if (result is Success<Recipe>) {
  final body = result.value;
  recipeDetail = body;
  if (mounted) {
    setState(() {});
  }
} else  {
  logMessage('Problems getting Recipe $result');
}

with:

final result = response.body;
if (result is Success<Recipe>) {
  final body = result.value;
  recipeDetail = body;
  if (mounted) {
    setState(() {});
  }
} else  {
  logMessage('Problems getting Recipe $result');
}

This will retrieve the recipe detail.

In readRecipe(), replace:

final result = snapshot.data;
if (result is Success<Recipe>) {
  final body = result.value;
  recipeDetail = body;
}

with:

final result = snapshot.data?.body;
if (result is Success<Recipe>) {
  final body = result.value;
  recipeDetail = body;
}

Open recipe_list.dart and add:

import 'dart:collection';

In _buildRecipeLoader(), replace:

final result = snapshot.data;
// Hit an error
if (result is Error) {
  const errorMessage = 'Problems getting data';
  return const SliverFillRemaining(
    child: Center(
      child: Text(
        errorMessage,
        textAlign: TextAlign.center,
        style: TextStyle(fontSize: 18.0),
      ),
    ),
  );
}

with:

if (false == snapshot.data?.isSuccessful) {
  var errorMessage = 'Problems getting data';
  if (snapshot.data?.error != null &&
      snapshot.data?.error is LinkedHashMap) {
    final map = snapshot.data?.error as LinkedHashMap;
    errorMessage = map['message'];
  }
  return SliverFillRemaining(
    child: Center(
      child: Text(
        errorMessage,
        textAlign: TextAlign.center,
        style: const TextStyle(fontSize: 18.0),
      ),
    ),
  );
}
final result = snapshot.data?.body;
if (result == null || result is Error) {
  inErrorState = true;
  return _buildRecipeList(context, currentSearchList);
}

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

Now, replace the existing fetchData() with:

Future<RecipeResponse> fetchData() async {
  if (!newDataRequired && currentResponse != null) {
    return currentResponse!;
  }
  newDataRequired = false;
  final recipeService = ref.watch(serviceProvider);
  currentResponse = recipeService.queryRecipes(
      searchTextController.text.trim(), currentStartPosition, pageCount);
  return currentResponse!;
}

This will use the services queryRecipes() instead of the older HTTP call.

Stop the app, run it again and choose the search value chicken from the drop-down button. Verify that you see the recipes displayed in the UI.

Now, look in the Run window of Android Studio, where you’ll see lots of [log] INFO messages related to your network calls. This is a great way to see how your requests and responses look and figure out what’s causing problems.

You made it! You can now use Chopper to make calls to the server API and retrieve recipes.

Key Points

  • The http package is a simple-to-use set of methods for retrieving data from the internet.
  • The built-in json.decode() transforms JSON strings into a map of objects that you can use in your code.
  • 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.

Where to Go From Here?

You’ve learned how to retrieve data from the internet and parse it into data models. If you want to learn more about the HTTP package and get the latest version, go to https://pub.dev/packages/http.

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.

In the next chapter, you’ll learn about the important topic of state management.

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.
© 2026 Kodeco Inc.