Chapters

Hide chapters

Flutter Apprentice

Second Edition · Flutter 2.5.1 · Dart 2.14.2 · Android Studio 2020.3.1

Section IV: Networking, Persistence and State

Section 4: 7 chapters
Show chapters Hide chapters

Appendices

Section 7: 2 chapters
Show chapters Hide chapters

10. Serialization With JSON
Written by Kevin D Moore

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In this chapter, you’ll learn how to serialize JSON data into model classes. A model class represents data for a particular object. An example is a recipe model class, which usually has a title, an ingredient list and steps to cook it.

You’ll continue with the previous project, which is the starter project for this chapter, and you’ll add a class that models a recipe and its properties. Then you’ll integrate that class into the existing project.

By the end of the chapter, you’ll know:

  • How to serialize JSON into model classes.
  • How to use Dart tools to automate the generation of model classes from JSON.

What is JSON?

JSON, which stands for JavaScript Object Notation, is an open-standard format used on the web and in mobile clients. It’s the most widely used format for Representational State Transfer (REST)-based APIs that servers provide (https://en.wikipedia.org/wiki/Representational_state_transfer). If you talk to a server that has a REST API, it will most likely return data in a JSON format. An example of a JSON response looks something like this:

{
  "recipe": {
    "uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_b79327d05b8e5b838ad6cfd9576b30b6",
    "label": "Chicken Vesuvio"
  }
}

That is an example recipe response that contains two fields inside a recipe object.

While it’s possible to treat the JSON as just a long string and try to parse out the data, it’s much easier to use a package that already knows how to do that. Flutter has a built-in package for decoding JSON, but in this chapter, you’ll use the json_serializable and json_annotation packages to help make the process easier.

Flutter’s built-in dart:convert package contains methods like json.decode and json.encode, which converts a JSON string to a Map<String, dynamic> and back. While this is a step ahead of manually parsing JSON, you’d still have to write extra code that takes that map and puts the values into a new class.

The json_serializable package comes in handy because it can generate model classes for you according to the annotations you provide via json_annotation. Before taking a look at automated serialization, you’ll see in the next section what manual serialization entails.

Writing the code yourself

So how do you go about writing code to serialize JSON yourself? Typical model classes have toJson() and fromJson() methods.

class Recipe {
  final String uri;
  final String label;

  Recipe({this.uri, this.label});
}
factory Recipe.fromJson(Map<String, dynamic> json) {
  return Recipe(json['uri'] as String, json['label'] as String);
}

Map<String, dynamic> toJson() {
  return <String, dynamic>{ 'uri': uri, 'label': label}
}

Automating JSON serialization

You’ll use two packages in this chapter: json_annotation and json_serializable from Google.

Adding the necessary dependencies

Continue with your current project or open the starter project in the projects folder. Add the following package to pubspec.yaml in the Flutter dependencies section underneath and aligned with shared_preferences:

json_annotation: ^4.1.0
build_runner: ^2.1.1
json_serializable: ^4.1.4
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.3
  cached_network_image: ^3.1.0
  flutter_slidable: ^0.6.0
  flutter_svg: ^0.22.0
  shared_preferences: ^2.0.7
  json_annotation: ^4.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  build_runner: ^2.1.1
  json_serializable: ^4.1.4

Generating classes from JSON

The JSON that you’re trying to serialize looks something like:

{
  "q": "pasta",
  "from": 0,
  "to": 10,
  "more": true,
  "count": 33060,
  "hits": [
    {
      "recipe": {
        "uri": "http://www.edamam.com/ontologies/edamam.owl#recipe_09b4dbdf0c7244c462a4d2622d88958e",
        "label": "Pasta Frittata Recipe",
        "image": "https://www.edamam.com/web-img/5a5/5a5220b7a65c911a1480502ed0532b5c.jpg",
        "source": "Food Republic",
        "url": "http://www.foodrepublic.com/2012/01/21/pasta-frittata-recipe",
    }
  ]
}

Creating model classes

Start by creating a new directory named network in the lib folder. Inside this folder, create a new file named recipe_model.dart. Then add the needed imports:

import 'package:json_annotation/json_annotation.dart';

part 'recipe_model.g.dart';
@JsonSerializable()
class APIRecipeQuery {
  // TODO: Add APIRecipeQuery.fromJson
}

// TODO: Add @JsonSerializable() class APIHits
// TODO: Add @JsonSerializable() class APIRecipe
// TODO: Add @JsonSerializable() class APIIngredients
/// If a field is annotated with `JsonKey` with a non-`null` value for
/// `includeIfNull`, that value takes precedent.
final bool? includeIfNull;

/// Creates a new [JsonSerializable] instance.
const JsonSerializable({
  @Deprecated('Has no effect') bool? nullable,
  this.anyMap,
  this.checked,
  this.createFactory,
  this.createToJson,
  this.disallowUnrecognizedKeys,
  this.explicitToJson,
  this.fieldRename,
  this.ignoreUnannotated,
  this.includeIfNull,
  this.genericArgumentFactories,
});

Converting to and from JSON

Now, you need to add JSON conversion methods within the APIRecipeQuery class. Return to recipe_model.dart and replace // TODO: Add APIRecipeQuery.fromJson with:

factory APIRecipeQuery.fromJson(Map<String, dynamic> json) =>
  _$APIRecipeQueryFromJson(json);

Map<String, dynamic> toJson() => _$APIRecipeQueryToJson(this);
@JsonKey(name: 'q')
String query;
int from;
int to;
bool more;
int count;
List<APIHits> hits;
APIRecipeQuery({
  required this.query,
  required this.from,
  required this.to,
  required this.more,
  required this.count,
  required this.hits,
});
// 1
@JsonSerializable()
class APIHits {
  // 2
  APIRecipe recipe;

  // 3
  APIHits({
    required this.recipe,
  });

  // 4
  factory APIHits.fromJson(Map<String, dynamic> json) =>
      _$APIHitsFromJson(json);
  Map<String, dynamic> toJson() => _$APIHitsToJson(this);
}
@JsonSerializable()
class APIRecipe {
  // 1
  String label;
  String image;
  String url;
  // 2
  List<APIIngredients> ingredients;
  double calories;
  double totalWeight;
  double totalTime;

  APIRecipe({
    required this.label,
    required this.image,
    required this.url,
    required this.ingredients,
    required this.calories,
    required this.totalWeight,
    required this.totalTime,
  });

  // 3
  factory APIRecipe.fromJson(Map<String, dynamic> json) =>
      _$APIRecipeFromJson(json);
  Map<String, dynamic> toJson() => _$APIRecipeToJson(this);
}

// TODO: Add global Helper Functions
// 4
String getCalories(double? calories) {
  if (calories == null) {
    return '0 KCAL';
  }
   return calories.floor().toString() + ' KCAL';
}

 // 5
 String getWeight(double? weight) {
   if (weight == null) {
     return '0g';
   }
   return weight.floor().toString() + 'g';
}
@JsonSerializable()
class APIIngredients {
  // 1
  @JsonKey(name: 'text')
  String name;
  double weight;

  APIIngredients({
    required this.name,
    required this.weight,
  });

  // 2
  factory APIIngredients.fromJson(Map<String, dynamic> json) =>
      _$APIIngredientsFromJson(json);
  Map<String, dynamic> toJson() => _$APIIngredientsToJson(this);
}

Generating the .part file

Open the terminal in Android Studio by clicking on the Terminal panel in the lower left, or by selecting View ▸ Tool Windows ▸ Terminal, and type:

flutter pub run build_runner build
[INFO] Generating build script...
...
[INFO] Creating build script snapshot......
...
[INFO] Running build...
...
[INFO] Succeeded after ...
flutter pub run build_runner watch
// 1
APIRecipeQuery _$APIRecipeQueryFromJson(Map<String, dynamic> json) =>
    APIRecipeQuery(
      // 2
      query: json['q'] as String,
      // 3
      from: json['from'] as int,
      to: json['to'] as int,
      more: json['more'] as bool,
      count: json['count'] as int,
      // 4
      hits: (json['hits'] as List<dynamic>)
          .map((e) => APIHits.fromJson(e as Map<String, dynamic>))
          .toList(),
    );

Testing the generated JSON code

Now that you can parse model objects from JSON, you’ll read one of the JSON files included in the starter project and show one card to make sure you can use the generated code.

import 'dart:convert';
import '../../network/recipe_model.dart';
import 'package:flutter/services.dart';
import '../recipe_card.dart';
import 'recipe_details.dart';
APIRecipeQuery? _currentRecipes1 = null;
Future loadRecipes() async {
  // 1
  final jsonString = await rootBundle.loadString('assets/recipes1.json');
  setState(() {
    // 2
    _currentRecipes1 = APIRecipeQuery.fromJson(jsonDecode(jsonString));
  });
}
loadRecipes();
Widget _buildRecipeCard(
  BuildContext topLevelContext, List<APIHits> hits, int index) {
  // 1
  final recipe = hits[index].recipe;
  return GestureDetector(
    onTap: () {
      Navigator.push(topLevelContext, MaterialPageRoute(
        builder: (context) {
          return const RecipeDetails();
        },
      ));
    },
    // 2
    child: recipeStringCard(recipe.image, recipe.label),
  );
}
Widget _buildRecipeLoader(BuildContext context) {
  // 1
  if (_currentRecipes1 == null || _currentRecipes1?.hits == null) {
    return Container();
  }
  // Show a loading indicator while waiting for the recipes
  return Center(
    // 2
    child: _buildRecipeCard(context, _currentRecipes1!.hits, 0),
  );
}

Key points

  • JSON is an open-standard format used on the web and in mobile clients, especially with REST APIs.
  • In mobile apps, JSON code is usually parsed into the model objects that your app will work with.
  • You can write JSON parsing code yourself, but it’s usually easier to let a JSON package generate the parsing code for you.
  • json_annotation and json_serializable are packages that will let you generate the parsing code.

Where to go from here?

In this chapter, you’ve learned how to create models that you can parse from JSON and then use when you fetch JSON data from the network. If you want to learn more about json_serializable, go to https://pub.dev/packages/json_serializable.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now