Chapters

Hide chapters

Flutter Apprentice

First Edition - Early Access 2 · Flutter 1.20 · Dart 2.9 · AS 4.0.1

Section III: Navigating Between Screens

Section 3: 3 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 Get dependencies button or execute flutter pub get from Terminal.

By the end of the 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.

With no further ado, it’s time to get started!

Signing up with the recipe API

For your remote content, you’ll use the Edamam Recipe API. Open this link in your browser: https://developer.edamam.com/.

Click the SIGN UP button at the top-right and choose the Recipe Search API option.

The page will display multiple subscription choices. Choose the free option by clicking the START NOW button in the Developer column:

On the Sign Up Info pop-up window, enter your information and click SIGN UP. You’ll receive an email confirmation shortly.

Once you’ve received the email and verified your account, return to the site and sign in. On the menu bar, click the Get an API key now! button:

Next, click the Create a new application button.

On the Select service page, click the Recipe Search API link.

A New Application page will come up. Enter raywenderlich.com Recipes for the app’s name and An app to display raywenderlich.com recipes as the description — or use any values you prefer. When you’re done, press the Create Application button.

Once the site generates the API key, you’ll see a screen with your Application ID and Application Key.

You‘ll need your API Key and ID later, so save them somewhere handy or keep the browser tab open. Now, check the API documentation, which provides important information about the API including paths, parameters and returned data.

Accessing the API documentation

At the top of the window, right-click the API Developer Portal link and select Open Link in New Tab.

In the new tab, click the Documentation menu and choose Recipe Search API.

This page has a wealth of information about the API you’re going to use. At the top, you‘ll see the Path and a list of the parameters available to use 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.

Using your API key

For your next step, you’ll need to use your newly created API key.

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

If you closed your browser, sign in again. Click the Dashboard button, then choose Applications from the menu bar. You’ll see something like this:

Click the View button to see your ID and key(s):

Keep this page open so you can copy the values into your code. Your first step is to import a handy package to perform HTTP requests.

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: ^0.12.2

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

Using the HTTP package

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

  • GET: Retrieves data.
  • POST: Saves 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, you don’t need to send headers.

Connecting to the recipe service

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

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

import 'package:http/http.dart';

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

const String apiId = '<Your API id>';
const String apiKey = '<Your API key>';
const String apiUrl = 'https://api.edamam.com/search';

Copy the API ID and key from your Edamam account and replace the existing apiKey and apiId assigned strings with your values. Do not copy the ending spaces and dash shown below:

The apiUrl variable holds the URL for the Edamam search API, from the recipe API documentation.

Still in recipe_service.dart add the following function to get the data from the API:

// 1
Future getData(String url) async {
    // 2
    print('Calling url: $url');
    // 3
    Response response = await get(url);
    // 4
    if (response.statusCode == 200) {
      // 5
      return response.body;
    } else {
      // 6
      print(response.statusCode);
    }
  }

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

  1. getData returns a Future (with an upper case “F”) because an API’s returned data type is determined in the future (lower case “f”). async signifies this method is an asynchronous operation.
  2. For debugging purposes, you print out the passed-in URL.
  3. response doesn’t have a value until await completes. Response and get are from the HTTP package. get fetches data from the provided url.
  4. A statusCode of 200 means the request was successful.
  5. You return the results embedded in response.body.
  6. Otherwise, you have an error — print the statusCode to the console.

Now, add this service class after getData():

class RecipeService {
  // 1
  Future<dynamic> getRecipes(String query, int from, int to) async {
    // 2
    var recipeData = await getData('$apiUrl?app_id=$apiId&app_key=$apiKey&q=$query&from=$from&to=$to');
    // 3
    return recipeData;
  }
}

In this code, you:

  1. Create a new method, getRecipes, with the parameters query, from and to. These let you get specific pages from the complete query. from starts at 0 and to is calculated by adding the from index to your page size. You use type Future<dynamic> for this method because you don‘t know which data type it will return or when it will finish. async signals that this method runs asynchronously.
  2. Use var to create a dynamic-type variable since you might not know the data type in advance. 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 (plus the IDs previously created in the Edamam dashboard).
  3. return the data retrieved from the API.

Note: This method doesn’t handle errors. You’ll learn how to address those in Chapter 13, “Using the Chopper library”.

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

Building the user interface

Every good collection of recipes starts with a recipe card, so you’ll build that first.

Creating the recipe card

The file recipe_card.dart contains a few methods for creating a card for your recipes. Open it now and add the following import:

import '../network/recipe_model.dart';

Now, change the line below // TODO: Replace with new class with:

Widget recipeCard(APIRecipe recipe) {

This creates a card using APIRecipe, but you’ll notice some red squiggles indicating that there are errors. To correct these, replace the line below // TODO: Replace with image from recipe with:

imageUrl: recipe.image,

and replace the line below // TODO: Replace with label from recipe with:

recipe.label,

Finally, open recipe_list.dart and replace the line below // TODO: Replace with new card method with:

child: recipeCard(recipe),

No more red squiggles, and now your stomach is growling. It’s time to see some recipes :]

Adding a recipe list

Your next step is to create a way for your users to find which card they want to try: a recipe list.

Still in recipe_list.dart, after the last import, add:

import '../../network/recipe_service.dart';

Replace the line below // TODO: Replace with new API class with:

List<APIHits> currentSearchList = List<APIHits>();

You’re getting close to running the app. Hang in there! It’s time to use the recipe service.

Retrieving recipe data

In recipe_list.dart, you need to create a method to get the data from RecipeService. You’ll pass in a query along with the starting and ending positions and the API will return the decoded JSON results.

After initState(), add:

// 1
Future<APIRecipeQuery> getRecipeData(String query, int from, int to) async {
	  // 2
    var recipeJson = await RecipeService().getRecipes(query, from, to);
  	// 3
    var recipeMap = json.decode(recipeJson);
    // 4
    return APIRecipeQuery.fromJson(recipeMap);;
}

Here’s what this does:

  1. The method is asynchronous and returns a Future. It takes a query and the start and the end positions of the recipe data, which from and to represent, respectively.
  2. You define recipeJson, which stores the results from RecipeService().getRecipes() after it finishes. It uses the from and to you created in step 1.
  3. The variable recipeMap uses Dart’s json.decode() to decode the string into a map of type Map<String, dynamic>.
  4. You use the JSON parsing method you created in the previous chapter to create an APIRecipeQuery model.

Now that you’ve created a way to get the data, it’s time to put it to use. After _buildRecipeLoader(), add the following:

// 1
Widget _buildRecipeList(BuildContext recipeListContext, List<APIHits> hits) {
  // 2
  var size = MediaQuery.of(context).size;
  final double itemHeight = 310;
  final double itemWidth = size.width / 2;
  // 3
  return Flexible(
    // 4
    child: GridView.builder(
      // 5
      controller: _scrollController,
      // 6
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: (itemWidth / itemHeight),
      ),
      // 7
      itemCount: hits.length,
      // 8
      itemBuilder: (BuildContext context, int index) {
        return _buildRecipeCard(recipeListContext, hits, index);
      },
    ),
  );
}

Here’s what’s going on:

  1. This method returns a widget and takes recipeListContext and a list of recipe hits.
  2. You use MediaQuery to get the device’s screen size. You then set a fixed image height and create two columns of cards whose width is half the device’s width.
  3. You return a widget that’s flexible in width and height.
  4. GridView is similar to ListView, but it allows for some interesting combinations of rows and columns. In this case, you use GridView.builder() because you know the number of items and you’ll use an itemBuilder.
  5. You use _scrollController, created in initState(), to detect when scrolling gets to about 70% from the bottom.
  6. The SliverGridDelegateWithFixedCrossAxisCount delegate has two columns and sets the aspect ratio.
  7. The length of your grid items depends on the number of items in the hits list.
  8. itemBuilder now uses _buildRecipeCard() to return a card for each recipe. _buildRecipeCard() retrieves the recipe from the hits list by using hits[index].recipe.

Great, now it’s time for a little housekeeping.

Removing the sample code

In the previous chapter, you added code to recipe_list.dart to show a single card. Now that you’re showing a list of cards, you need to clean up some of the existing code to use the new API.

At the top of _RecipeListState, remove this variable declaration:

 APIRecipeQuery _currentRecipes1;

In initState(), remove the call to loadRecipes() and find and remove the declaration of loadRecipes().

Replace the existing _buildRecipeLoader() with the code below. Ignore any warning squiggles in the code for now:

Widget _buildRecipeLoader(BuildContext context) {
    // 1
    if (searchTextController.text.length < 3) {
      return Container();
    }
    // 2
    return FutureBuilder<APIRecipeQuery>(
        // 3
        future: getRecipeData(searchTextController.text.trim(), currentStartPosition, currentEndPosition),
        // 4
        builder: (context, snapshot) {
          // 5
          if (snapshot.connectionState == ConnectionState.done) {
            // 6
            if (snapshot.hasError) {
              return Center(
                child: Text(snapshot.error.toString(), textAlign: TextAlign.center, textScaleFactor: 1.3),
              );
            }

            // 7
            loading = false;
            final query = snapshot.data;
            inErrorState = false;
            currentCount = query.count;
            hasMore = query.more;
            currentSearchList.addAll(query.hits);
            // 8
            if (query.to < currentEndPosition) {
              currentEndPosition = query.to;
            }
            // 9
            return _buildRecipeList(context, currentSearchList);
          }
          // TODO: Handle not done connection
      },
    );
  }

Here’s what’s going on:

  1. You check there are at least three characters in the search term. You can change this value, but you probably won’t get good results with only one or two characters.
  2. FutureBuilder determines the current state of the Future that APIRecipeQuery returns. It then builds a widget that displays asynchronous data while it’s loading.
  3. You assign the Future that getRecipeData returns to future.
  4. builder is required; it returns a widget.
  5. You check the connectionState. If the state is done, you can update the UI with the results or an error.
  6. If there’s an error, return a simple Text element that displays the error message.
  7. If there’s no error, process the query results and add query.hits to currentSearchList.
  8. If you aren’t at the end of the data, set currentEndPosition to the current location.
  9. Return _buildRecipeList() using currentSearchList.

For your next step, you’ll handle the case where snapshot.connectionState isn’t complete.

Replace // TODO: Handle not done connection with the following:

// 10
else {
  // 11
  if (currentCount == 0) {
    // Show a loading indicator while waiting for the movies
    return Center(child: CircularProgressIndicator());
  } else {
    // 12
    return _buildRecipeList(context, currentSearchList);
  }
}

Walking through this, step-by-step:

  1. You check that snapshot.connectionState isn’t done.
  2. If the current count is 0, show a progress indicator.
  3. Otherwise, just show the current list.

Note: If you need a refresher on scrolling, check out Chapter 5, “Scrollable Widgets”.

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

Perform a Hot reload, if needed. Type Chicken in the text field and press the Search icon. While the app pulls data from the API, you’ll see the circular progress bar:

After the app receives the data, you’ll see a grid 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 show your friends what you’ve created. :]

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

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.
  • FutureBuilder is a widget that retrieves information from a Future.
  • GridView is useful for displaying columns of data.

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.

In the next chapter, you’ll learn about the Chopper package, which will make handling data from the internet even easier. Till then!

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.