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 getfrom 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:
-
getDatareturns aFuture(with an upper case “F”) because an API’s returned data type is determined in the future (lower case “f”).asyncsignifies this method is an asynchronous operation. - For debugging purposes, you print out the passed-in URL.
-
responsedoesn’t have a value untilawaitcompletes.Responseandgetare from the HTTP package.getfetches data from the providedurl. - A
statusCodeof 200 means the request was successful. - You
returnthe results embedded inresponse.body. - Otherwise, you have an error — print the
statusCodeto 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:
- Create a new method,
getRecipes, with the parametersquery,fromandto. These let you get specific pages from the complete query.fromstarts at 0 andtois calculated by adding thefromindex 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.asyncsignals that this method runs asynchronously. - Use
varto create a dynamic-type variable since you might not know the data type in advance. You useawaitto tell the app to wait untilgetDatareturns 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). -
returnthe 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:
- The method is asynchronous and returns a
Future. It takes aqueryand the start and the end positions of the recipe data, whichfromandtorepresent, respectively. - You define
recipeJson, which stores the results fromRecipeService().getRecipes()after it finishes. It uses thefromandtoyou created in step 1. - The variable
recipeMapuses 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
APIRecipeQuerymodel.
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:
- This method returns a widget and takes
recipeListContextand a list of recipehits. - You use
MediaQueryto 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. - You return a widget that’s flexible in width and height.
-
GridViewis 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
SliverGridDelegateWithFixedCrossAxisCountdelegate has two columns and sets the aspect ratio. - The length of your grid items depends on the number of items in the
hitslist. -
itemBuildernow 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;
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:
- 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.
-
FutureBuilderdetermines the current state of theFuturethatAPIRecipeQueryreturns. It then builds a widget that displays asynchronous data while it’s loading. - You assign the
FuturethatgetRecipeDatareturns tofuture. -
builderis 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
Textelement that displays the error message. - If there’s no error, process the query results and add
query.hitstocurrentSearchList. - If you aren’t at the end of the data, set
currentEndPositionto 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 movies
return Center(child: CircularProgressIndicator());
} else {
// 12
return _buildRecipeList(context, currentSearchList);
}
}
Walking through this, step-by-step:
- You check that
snapshot.connectionStateisn’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.decodetransforms JSON strings into a map of objects that you can use in your code. -
FutureBuilderis a widget that retrieves information from aFuture. -
GridViewis 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!