跳转至

11 Networking in FlutterWritten 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://www.edamam.com/.

Click the Signup API button at the top-right to create an account.

img

Fill in a username, email, password, organization, in the Choose your plan dropdown choose Recipe Search API -> Developer, read the Term and Privacy Policy, check the I have read and agree box and press Submit.

img

Next sign in by entering the username and password you created earlier and press Submit.

img

The main page will now show the APIs button at the top middle. Select the Recipe Search API.

img

Next select the Get Started button under the Developer column:

img

You’ll see the Sign Up dialog again. At the bottom, choose Login.

img

On the next screen, select Go to Dashboard.

img

Now click on the Applications tab and then the Create a new application.

img

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

img

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.

Note: As with any API, be sure you’re OK with the API’s Legal Terms and Conditions.

When you’re done, press the Create Application button.

img

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

img

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, click on APIs, right-click on the Recipe Search API link and select Open Link in New Tab*.

img

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

img

Note: The Edamam website just recently added a new V2 version of the API. In this chapter you’ll use V1.

You will see the screen below. Since this chapter covers the V1 version of the API, click on the older version link.

img

Next, scroll down and you will see a wealth of information about the API you’re going to use.

In the Interfaces section, you’ll see Path and a list of the parameters available to use for the GET request you’ll make.

img

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

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, and you don’t need to send headers.

Connecting to the recipe service

To fetch data from the recipe API, you’ll create a Dart class to manage the connection. This Dart class 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 'dart:developer';
import 'package:http/http.dart';

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

const String apiKey = '<Your Key>';
const String apiId = '<your ID>';
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:

img

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

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

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

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. response doesn’t have a value until await completes. 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, you have an error — print the statusCode to the console.

Now, replace // TODO: Add getRecipes with:

// 1
Future<dynamic> getRecipes(String query, int from, int to) async {
  // 2
  final 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 final to create 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 (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 12, “Using a Network 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 ui/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';
import 'package:cached_network_image/cached_network_image.dart';

Now, find// TODO: Replace with new class and replace it and the line beneath it:

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, find // TODO: Replace with image from recipe and replace:

child: Image.asset(
    'assets/images/pizza_w700.png',
     height: 200,
     width: 200,
),

with:

child: CachedNetworkImage(
    imageUrl: recipe.image, height: 210, fit: BoxFit.fill),

The CachedNetworkImage package will load a network image and cache it for fast reloading.

You’re now using an image from the recipe.

To use the label returned with the recipe, locate// TODO: Replace with label from recipeand replace it and the line below with:

recipe.label,

Now, to use getCalories() created in the previous chapter, find and replace // TODO: Replace Padding section with getCalories() and Padding() call beneath it with:

Padding(
  padding: const EdgeInsets.only(left: 8.0),
  child: Text(
    getCalories(recipe.calories),
    style: const TextStyle(
      fontWeight: FontWeight.normal,
      fontSize: 11,
    ),
  ),
),

Finally, open ui/recipes/recipe_list.dart and at the bottom replace // TODO: Replace with recipeCard and the line below it 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 recipe they want to try: a recipe list.

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

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

At // TODO: Replace with new API class replace it and the List currentSearchList = []; line beneath it with:

List<APIHits> currentSearchList = [];

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

Retrieving recipe data

Still 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.

Find // TODO: Add getRecipeData() here and replace it with this new method:

// 1
Future<APIRecipeQuery> getRecipeData(String query, int from, int to) async {
      // 2
    final recipeJson = await RecipeService().getRecipes(query, from, to);
    // 3
    final 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 getRecipes() after it finishes. It uses the from and to fields from 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. Find and replace // TODO: Add _buildRecipeList() with the following:

// 1
Widget _buildRecipeList(BuildContext recipeListContext, List<APIHits> hits) {
  // 2
  final size = MediaQuery.of(context).size;
  const itemHeight = 310;
  final 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 item 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;

Find // TODO: Remove call to loadRecipes() and remove it and the next line that calls loadRecipes().

Now, find // TODO: Delete loadRecipes() and remove it and loadRecipes().

Locate // TODO: Replace this _buildRecipeLoader definition and replace the existing _buildRecipeLoader() with the code below. Ignore any red 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;
        if (query != null) {
          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 APIRecipeQueryreturns. 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 recipes
    return const 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:

img

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

img

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!