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.
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.
Next sign in by entering the username and password you created earlier and press Submit.
The main page will now show the APIs button at the top middle. Select the Recipe Search API.
Next select the Get Started button under the Developer column:
You’ll see the Sign Up dialog again. At the bottom, choose Login.
On the next screen, select Go to Dashboard.
Now click on the Applications tab and then the Create a new application.
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.
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.
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, click on APIs, right-click on the Recipe Search API link and select Open Link in New Tab*.
In the new tab, click the Documentation menu and choose Recipe Search API.
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.
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.
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:
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:
getData()
returns aFuture
(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.response
doesn’t have a value untilawait
completes.Response
andget()
are from the HTTP package.get()
fetches data from the providedurl
.- A
statusCode
of 200 means the request was successful. - You
return
the results embedded inresponse.body
. - 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:
- Create a new method,
getRecipes()
, with the parametersquery
,from
andto
. These let you get specific pages from the complete query.from
starts at 0 andto
is calculated by adding thefrom
index to your page size. You use typeFuture<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. - Use
final
to create a non-changing variable. You useawait
to tell the app to wait untilgetData()
returns its result. Look closely atgetData()
and note that you’re creating the API URL with the variables passed in (plus the IDs previously created in the Edamam dashboard). 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 recipe
and 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:
- The method is asynchronous and returns a
Future
. It takes aquery
and the start and the end positions of the recipe data, whichfrom
andto
represent, respectively. - You define
recipeJson
, which stores the results fromgetRecipes()
after it finishes. It uses thefrom
andto
fields from step 1. - The variable
recipeMap
uses Dart’sjson.decode()
to decode the string into a map of typeMap<String, dynamic>
. - 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:
- This method returns a widget and takes
recipeListContext
and a list of recipehits
. - 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. - You return a widget that’s flexible in width and height.
GridView
is similar toListView
, but it allows for some interesting combinations of rows and columns. In this case, you useGridView.builder()
because you know the number of items and you’ll use anitemBuilder
.- You use
_scrollController
, created ininitState()
, to detect when scrolling gets to about 70% from the bottom. - The
SliverGridDelegateWithFixedCrossAxisCount
delegate has two columns and sets the aspect ratio. - The length of your grid items depends on the number of items in the
hits
list. itemBuilder
now uses_buildRecipeCard()
to return a card for each recipe._buildRecipeCard()
retrieves the recipe from the hits list by usinghits[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:
- 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.
FutureBuilder
determines the current state of theFuture
thatAPIRecipeQuery
returns. It then builds a widget that displays asynchronous data while it’s loading.- You assign the
Future
thatgetRecipeData()
returns tofuture
. builder
is required; it returns a widget.- You check the
connectionState
. If the state is done, you can update the UI with the results or an error. - If there’s an error, return a simple
Text
element that displays the error message. - If there’s no error, process the query results and add
query.hits
tocurrentSearchList
. - If you aren’t at the end of the data, set
currentEndPosition
to the current location. - Return
_buildRecipeList()
usingcurrentSearchList
.
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:
- You check that
snapshot.connectionState
isn’t done. - If the current count is 0, show a progress indicator.
- 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 aFuture
.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!