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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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

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:

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.

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';
const String apiKey = '<Add Your Key Here>';
const String apiUrl = 'https://api.spoonacular.com/';
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
}
// 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);
}

Updating the User Interface

Open main.dart and add the following:

import 'network/spoonacular_service.dart';
final service = await MockService.create();
final service = SpoonacularService();

Retrieving Recipe Data

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

Why Chopper?

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

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.

chopper: ^6.1.4
chopper_generator: ^6.0.3

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.

// 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);
}
import 'package:chopper/chopper.dart';
typedef RecipeResponse = Result<QueryResult>;
typedef RecipeDetailsResponse = Result<Recipe>;
typedef RecipeResponse = Response<Result<QueryResult>>;
typedef RecipeDetailsResponse = Response<Result<Recipe>>;
import 'package:http/http.dart' as http;
import 'package:chopper/chopper.dart';
return Future.value(
          Response(
            http.Response(
              'Dummy',
              200,
              request: null,
            ),
            Success<QueryResult>(_currentRecipes1),
          ),
        );
  return Future.value(
      Response(
        http.Response(
          'Dummy',
          200,
          request: null,
        ),
        Success<Recipe>(recipeDetails),
      ),
    );

Preparing the Recipe Service

Open spoonacular_service.dart.

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';
// 1
@ChopperApi()
// 2
abstract class SpoonacularService extends ChopperService
    implements ServiceInterface {

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

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.

import 'dart:convert';
import 'package:chopper/chopper.dart';
import 'model_response.dart';
import 'query_result.dart';
import 'spoonacular_model.dart';
// 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
}

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.

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

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.

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);
    }
}
@override
Response<BodyType> convertResponse<BodyType, InnerType>(Response response) {
  // 1
  return decodeJson<BodyType, InnerType>(response);
}

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.

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

Wiring Up Interceptors and Converters

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

import 'spoonacular_converter.dart';
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);
}

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.

dart run build_runner build --delete-conflicting-outputs

Using the Chopper Client

Open main.dart and after sharedPrefs, replace:

  final service = SpoonacularService();
final service = SpoonacularService.create();

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');
}
final result = response.body;
if (result is Success<Recipe>) {
  final body = result.value;
  recipeDetail = body;
  if (mounted) {
    setState(() {});
  }
} else  {
  logMessage('Problems getting Recipe $result');
}
final result = snapshot.data;
if (result is Success<Recipe>) {
  final body = result.value;
  recipeDetail = body;
}
final result = snapshot.data?.body;
if (result is Success<Recipe>) {
  final body = result.value;
  recipeDetail = body;
}
import 'dart:collection';
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),
      ),
    ),
  );
}
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);
}
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!;
}

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.

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

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now