Parsing JSON in Flutter | Kodeco

Update note: Sardor Islomov updated this tutorial for Flutter 3.7.5. Kevin Moore wrote the original.

An app without something to show on its screen is pretty boring, right? But where can you get interesting information to display in your app? From the internet, of course!

Thousands of websites can provide information that lets you spice up your apps through REST, or representational state transfer, APIs. These APIs define a way to implement web services. Many sites allow you to create an account to access resources like images, data and more through REST APIs.

In this tutorial, you’ll sign up for a website that provides information about cats, and you’ll build a Flutter app to display that information — and for you dog lovers out there, there are dog APIs as well. You’ll get a list of cat breeds along with information about each breed. That information also includes an image you can display that shows how each cat breed looks.

But once you get that information, how do you put it on your app’s screen? This tutorial will also show you how to parse JSON data into model classes you can display in your app. JSON stands for JavaScript Object Notation, a data format that most websites use to send data.

In this tutorial, you’ll see how Flutter implements the following:

  • Calling network APIs.
  • Parsing JSON data.
  • Showing JSON data in a ListView.
  • Displaying network images.
Note: If you’re new to Flutter, please check out our Getting Started With Flutter tutorial for an overview of the basics of working with this SDK.

Getting Started

Download the starter project for this tutorial by clicking Download materials at the top or bottom of the page.

This tutorial uses Android Studio with the Flutter plugin installed. However, you can also use Visual Studio Code, IntelliJ IDEA or a text editor of your choice with Flutter at the command line.

To install the Flutter plugin, open Android Studio and find the Plugins section.

Click the Marketplace tab and type Flutter, then click Install. You may also need to install other plugins like Dart, but installing the Flutter plugin should install the other needed plugins for you.

With the plugins installed, open the starter project in Android Studio by choosing Open an existing Android Studio project and finding the root folder of the starter project zip file.

Open flutter project

Select file

Android Studio may prompt you to fetch the packages needed for the project. If so, click Get dependencies.

Starter project get dependency

Once you’ve opened the project in Android Studio, in the device dropdown, select either an Android emulator or the iOS simulator if you’re on a Mac and have Xcode installed. Then, press Control-R or click the green Run button to build and run the starter project.

Run starter project

The starter app will show an empty screen like this:

iOS Start run result

In this tutorial, you’ll build on the starter project to first load a set of cat breeds with a short description of each breed. Then, you’ll update the list of breeds, so clicking on a row takes the user to an image of a cat from that breed.

Understanding the UI

Right now, you can’t see anything on the screen because your app has no data. You’ll start fixing that soon.

First, look at the code that builds the UI. Open cat_breeds.dart in the lib/screens folder, which contains the following code:

import 'package: <a href="https://yourselfhood.com/google-expands-its-ai-search-to-younger-users-offers-publishers-a-new-tool/"  class="lar_link" data-linkid="2710" data-postid="2200"  title="flutter"   target="_blank" >flutter</a>/material.dart';
import 'cat_info.dart';

class CatBreedsPage extends StatefulWidget {
  // 1
  const CatBreedsPage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  State<CatBreedsPage> createState() => _CatBreedsPageState();
}

class _CatBreedsPageState extends State<CatBreedsPage> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        // 2
        title: Text(widget.title),
      ),
      // 3
      body: ListView.builder(
          // 4
          itemCount: 0,
          itemBuilder: (context, index) {
            // 5
            return GestureDetector(
              onTap: () {
                Navigator.push<void>(context,
                    MaterialPageRoute(builder: (context) {
                  return CatInfo(catId: 'id', catBreed: 'Name');
                }));
              },
              // 6
              child: Card(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  // 7
                  child: ListTile(
                    title: Text('Breed Name'),
                    subtitle: Text('Breed Description'),
                  ),
                ),
              ),
            );
          }),
    );
  }
}

Here’s a description of what each section does:

  1. Constructs a CatBreedsPage with the title that the AppBar will use.
  2. Adds the title for the AppBar using the title field.
  3. Adds a body that uses the ListView.builder method.
  4. Sets the count to 0 for now since you don’t have any items yet.
  5. For every card, you want to go to the CatInfo page. Here, you use the Navigator to push that card.
  6. Creates a Card widget with padding.
  7. Adds a ListTile that has a title and description.

You’ll update this UI code once you’ve downloaded real data to show in it.

Using REST APIs

REST APIs consist of URLs to a server that allow you to save, modify and retrieve data.

You can use a few different HTTP methods with REST APIs. The most common method you’ll use is GET, which retrieves data rather than saving it. In addition, you can use POST for saving and PATCH or PUT for updating. There’s also a DELETE method for deleting.

If you go to the documentation page of the Cats API, you’ll see all of the different calls you can make. If you click the Search by Breed link, you’ll see that you need an API key to complete the action.

You can also see that the API looks like this: https://api.thecatapi.com/v1/images/search?breed_ids={breed-id} where {breed-id} stands for the ID of the breed to search.

Signing Up for the Cats API

Head over to the Cats API page and sign up for an account.

You must sign up and get a key since you’d only be able to make a few calls without the key.

Making Network Calls

Making network calls is easy in Dart. All you have to do is use the HTTP library from the starter project. Open network.dart in the lib/api folder. It looks like this:

// 1
import 'package:http/http.dart';

class Network {
  final String URL;
  // 2
  Network(this.url);
  // 3
  Future<String> getData() async {
    // 4
    final response = await get(Uri.parse(url));
    // 5
    if (response.statusCode == 200) {
      // 6
      return response.body;
    } else {
      return '';
    }
  }
}

Here’s what this class does:

  1. Imports the HTTP library.
  2. The Network class has a constructor that takes a string URL.
  3. Includes one asynchronous method called getData().
  4. Fetches cat data using the HTTP GET method with your URL and awaits a response.
  5. Checks the status code. If it’s 200, then the response is OK. Anything else is an error.
  6. Returns the result.

Understanding JSON

JSON is just a text format that most REST APIs use to return their data. Another common format is XML, but XML is quite a bit more verbose.

Here’s a small example of JSON:

{
  "user": {
      "name": "Sardor Islomov",
      "occupation": "Software Engineer"
   }
}

Notice that the snippet starts with a brace {, which indicates an object. JSON can also start as an array, which uses the square bracket [ to signify the start of the array. JSON needs to be properly formatted, so all beginning { and [ symbols need to have their ending symbols: } and ].

Once you’ve downloaded a JSON string, you can do a few different things:

  • Keep it as a string and parse out the key/value pairs.
  • Convert the string to a Dart Map from which you can get the key/value pairs.
  • Convert the string to Dart model objects from which you can get the values from the object properties.

All these options have different pros and cons. Dealing with a string can get complicated if you have a lot of data. Using Map values can make the code quite verbose. Converting to model objects takes more work but is easier to use.

You’ll use the model object approach below.

Parsing JSON

You can parse JSON code in a few different ways.

By Hand

You can parse a JSON string by hand by using the dart:convert library.

Here’s an example:

import 'dart:convert';

Map<String, dynamic> user = jsonDecode(jsonString);
var name = user['user']['name'];

This doesn’t look too hard, but if you start working with complex JSON strings, it becomes very tedious to write and maintain.

Using Libraries

If you go to Pub.dev, a repository of Dart packages, and search for JSON Flutter libraries, you’ll find several libraries for dealing with JSON. For this tutorial, you’ll use two Flutter libraries:

  • HTTP for network calls, as seen above.
  • json_annotation for annotating your JSON model classes.

You’ll also use two development libraries that create helper classes for converting JSON strings into your model objects:

  • build_runner, which runs your json_serializable library.
  • json_serializable, which creates the extra helper classes that convert strings into your models.

These two libraries will go into the dev_dependencies section of your pubspec.yaml.

To add them, start by opening pubspec.yaml in the root of the project. First, you’ll add the Flutter libraries.

In the dependencies section, add the json_annotation dependency underneath http:

dependencies:
   <a href="https://yourselfhood.com/google-expands-its-ai-search-to-younger-users-offers-publishers-a-new-tool/"  class="lar_link" data-linkid="2710" data-postid="2200"  title="flutter"   target="_blank" >flutter</a>:
    sdk:  <a href="https://yourselfhood.com/google-expands-its-ai-search-to-younger-users-offers-publishers-a-new-tool/"  class="lar_link" data-linkid="2710" data-postid="2200"  title="flutter"   target="_blank" >flutter</a>

  cupertino_icons: ^1.0.5
  http: ^0.13.6
  json_annotation: ^4.8.1

Next, go to the dev_dependencies section and add the build_runner and json_serializable dependencies:

dev_dependencies:
  flutter_test:
    sdk:  <a href="https://yourselfhood.com/google-expands-its-ai-search-to-younger-users-offers-publishers-a-new-tool/"  class="lar_link" data-linkid="2710" data-postid="2200"  title="flutter"   target="_blank" >flutter</a>
  build_runner: ^2.4.4
  json_serializable: ^6.7.0

Next, press the Packages get or the Pub get prompt that shows up in the top-right side of the Android Studio. If you have any problems, make sure you line up the dependencies to match what you see in the final project since YAML file formatting is very strict.

The Cat API

Now, open cats_api.dart in the lib/api folder. The first line is a constant called apiKey. Replace Your Key with the key you obtained from the Cat API site, then look at the code:

const String apiKey = '''Your Key''';
// 1
const String catAPIURL = 'https://api.thecatapi.com/v1/breeds?';
// 2
const String catImageAPIURL = 'https://api.thecatapi.com/v1/images/search?';
// 3
const String breedString = 'breed_id=';
// 4
const String apiKeyString = 'x-api-key=$apiKey';

class CatAPI {
  // 5
  Future<String> getCatBreeds() async {
    // 6
    final network = Network('$catAPIURL$apiKeyString');
    // 7
    final catData = await network.getData();
    return catData;
  }
  // 8
  Future<String> getCatBreed(String breedName) async {
    final network =
    Network('$catImageAPIURL$breedString$breedName&$apiKeyString');
    final catData = await network.getData();
    return catData;
  }
}

Here’s what you see:

  1. A string value of the API to get the list of breeds.
  2. The URL for running a cat image search.
  3. A string to capture the actual breed ID.
  4. A string that uses your API key to add to the final URL.
  5. The method getCatBreeds() to return the breed data.
  6. Use of your Network class from above to pass in your breed’s API string and your key.
  7. Awaiting the asynchronous result.
  8. A method getCatBreed(String breedName) to get the cat image for a given breed.

Using the Cat API

Open cat_breeds.dart in the lib/screens folder.

Inside _CatBreedsPageState, add the following:

void getCatData() async {
  final catJson = await CatAPI().getCatBreeds();
  print(catJson);
}

This method calls the Cat API to get the cat breeds.

You’ll need to import the CatAPI class from cat_info.dart. You can do that manually or, if you like, put the cursor over the call to the CatAPI constructor, press Option-Enter and choose Import.

Next, call the new method you’ve added to get the cat data by modifying initState() to the following:

@override
void initState() {
  super.initState();
  getCatData();
}

Now, run/restart your app to check if your connection to the API works. Look at the output in the run tab, and you’ll see the JSON string printed out:

Cat API Log Output

Now that your initial call to the Cat API works, you’ll create the model classes you need in the next step.

Creating Models

Get started by opening cats.dart in the lib/models folder. You’ll see commented out an example of the JSON data returned by the API.

Add a class that describes a cat breed:

class Breed {
  String id;
  String name;
  String description;
  String temperament;

  Breed({
    required this.id,
    required this.name,
    required this.description,
    required this.temperament
  });

}

This class defines the fields you’ll pull from the JSON. You need the id to get the image of the cat breed. You’ll display name and description on the card view.

Look at the data you printed to the console above, and you’ll see that it starts with the square bracket [ character, meaning you’ll get a JSON array of breeds. Add a class to hold that array of data now:

class BreedList {
  List<Breed> breeds;

  BreedList({required this.breeds});
}

This class holds a Dart list of cat breeds.

For the image search, you need to describe the cat, the cat breed and the list of cat breeds. Add the classes below to cats.dart:

class Cat {
  String name;
  String description;
  String life_span;

  Cat({required this.name,required this.description,required this.life_span});
}

class CatBreed {
  String id;
  String url;
  int width;
  int height;

  CatBreed({
    required this.id,
    required this.url,
    required this.width,
    required this.height
  });
}

class CatList {
  List<CatBreed> breeds;

  CatList({required this.breeds});
}

For this tutorial, you won’t use the temperament or life_span fields, but you could use them if you wanted to enhance the app.

Using JSON Annotations

Now, you’ll use the json_annotation library to parse the JSON data into objects of your model classes.

Go to the top of cats.dart, and add the following imports to the top:

import 'package:json_annotation/json_annotation.dart';
part 'cats.g.dart';

The part statement imports a file and allows you to use its private variables. You’ll see an error on this statement for now until you later use build_runner to generate the file cats.g.dart.

Next, you need to add the @JsonSerializable() annotation to each class in cats.dart. For example, your Breed class should look like this when you add the annotation:

@JsonSerializable()
class Breed {
  String id;
  String name;
  String description;
  String temperament;

  Breed({
    required this.id,
    required this.name,
    required this.description,
    required this.temperament
  });
}

Make sure you add the annotation before every class in cats.dart.

JSON Conversion Methods

In the next step, you’ll add some factory methods to each class. The build runner plugin will use these methods to create a Dart file to do all the hard work of parsing the JSON data for you.

In the Breed class, add the following after the constructor:

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

Map<String, dynamic> toJson() => _$BreedToJson(this);

Each class will include fromJson and toJson. These methods call the generated code that parses the JSON. At this point, you’ll notice some more errors in Android Studio. Don’t worry about these at the moment; you’ll clear them up later.

In BreedList, add the following after the constructor:

factory BreedList.fromJson(final dynamic json) {
  return BreedList(
      breeds: (json as List<dynamic>)
          .map((dynamic e) => Breed.fromJson(e as Map<String, dynamic>))
          .toList());
}

This is the fromJson method you need to parse the JSON array to a list of breeds.

Add fromJson and toJson after the constructor in Cat:

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

Map<String, dynamic> toJson() => _$CatToJson(this);

Next, after the constructor in CatBreed, add:

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

Map<String, dynamic> toJson() => _$CatBreedToJson(this);

Finally, add the following after the constructor in CatList:

factory CatList.fromJson(dynamic json) {
  return CatList(
      breeds: (json as List<dynamic>)
          .map((dynamic e) => CatBreed.fromJson(e as Map<String, dynamic>))
          .toList());
}

You’ve now added all the fromJson and toJson methods you need in your model classes.

Using build_runner

Your next step is to run the tool that generates the files that will parse the JSON. Open the Terminal tab at the bottom of Android Studio, and enter the following:

dart run build_runner build

When the command completes, if everything ran correctly, the errors you saw earlier in cats.dart will be gone. You’ll now see cats.g.dart in the same directory as cats.dart. If you open cats.g.dart, you’ll notice methods for converting JSON to your model classes and back.

Error Handling

Developers should handle unexpected values from JSON objects. For example, you expect a string type, but the server returns null. This isn’t a rare case where you should leave it as it is. Check the code below:

@JsonSerializable()
class CatBreed {
  String id;
  String url;
  int width;
  int height;

  CatBreed({
    required this.id,
    required this.url,
    required this.width,
    required this.height
  });

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

  Map<String, dynamic> toJson() => _$CatBreedToJson(this);
}

Cat image, in this case String url, could be null. To avoid any NullPointerException, pass an empty string when String url is null.

You could modify CatBreed.fromJson() to the following:

  factory CatBreed.fromJson(Map<String, dynamic> json) {
    // 1
    try {
      final id =  json['id'] as String;
      final url =  json['url'] as String;
      final width = json['width'] as int;
      final height = json['height'] as int;
      return CatBreed(
        id: id,
        url: url,
        width: width,
        height: height,
      );
    } catch(e) {
      // 2
      return CatBreed(
        id: '',
        url: '',
        width: -1,
        height: -1,
      );
    }
  }

In the code above:

  1. Wraps the fromJson method with the try block to catch any cast exceptions.
  2. The catch block returns a default CatBreed object with all properties being default values.

The code above looks OK, but not elegant. The main drawback of this approach is that if try throws an exception for one property, all properties will be created as a default. The developer doesn’t understand which property is causing the problem.

To fix that, modify CatBreed.fromJson() to the following:

factory CatBreed.fromJson(Map<String, dynamic> json) {
  return CatBreed(
     id: tryCast<String>(json['id']) ?? '',
     url: tryCast<String>(json['url']) ?? '',
     width: tryCast<int>(json['width']) ?? 0,
     height: tryCast<int>(json['height']) ?? 0,
  );
}

Here, you create and return the CatBreed object with default values using the null-coalescing operator (??).

Next, add the tryCast method at the end of cats.dart.

T? tryCast<T>(dynamic object) => object is T ? object : null;

tryCast is a simple method that tries to cast an object into a given type, and if it’s unsuccessful, it returns null.

Now, the code looks elegant, small and easy to read. In the coming sections, you’ll connect the UI with a network response.

Using the Models

Now that you’ve created and generated your models, it’s time to put them to work.

Go back to cat_breeds.dart. In getCatData(), you can now parse the JSON you got from the internet into your model classes.

To start, at the top of _CatBreedsPageState, add a property for the breed list:

class _CatBreedsPageState extends State<CatBreedsPage> {
  BreedList breedList = BreedList(breeds: List.empty());
  ...

Add the import import '../models/cats.dart'; at the top of the file to clear the errors you see.

In getCatData(), add these lines after the print statement:

// 1
final dynamic catMap = json.decode(catJson);
// 2
setState(() {
  // 3
  breedList = BreedList.fromJson(catMap);
});

Here, you:

  1. Use json.decode(catJson) to turn the JSON string into a map.
  2. Call setState to rebuild your widget due to changes in the data.
  3. Use BreedList.fromJson(catMap) to convert the map into a list of breeds.

Be sure to import the dart:convert library(import 'dart:convert';) for the json.decode() statement. You’ve now converted your JSON data into a list of cat breeds!

But wait! You still need to get that list into the UI. How do you do that?

Since you have a list of cat breeds, what better way to display them than with a ListView widget?

Go down to the body: ListView.builder statement and replace itemCount: 0 with:

itemCount: breedList.breeds.length,

This sets itemCount to the number of cat breeds you got from the internet.

Next, replace title and subtitle of ListTile with the following:

title: Text(breedList.breeds[index].name),
subtitle: Text(breedList.breeds[index].description),

Now, build and run the app, and see how it looks. You’ll see a list of cat breed names and their descriptions:

Cat breed list

Congratulations!

Happy Cat

Building the Cat Detail Page

Your next step is to set up the onTap listener so that tapping a row shows the breed image.

Replace the code in the onTap() property of GestureDetector with the following:

Navigator.push<void>(context,
    MaterialPageRoute(builder: (context) {
      return CatInfo(
          catId: breedList.breeds[index].id, 
          catBreed: breedList.breeds[index].name,
      );
}));

This adds the actual id and name of the tapped row into the constructor call for CatInfo.

Now, open cat_info.dart in lib/screens. In _CatInfoState, add the following code above the initState override:

CatList catList = CatList(breeds: List.empty());

void getCatData() async {
  final catJson = await CatAPI().getCatBreed(widget.catId);

  final dynamic catMap = json.decode(catJson);

  setState(() {
    catList = CatList.fromJson(catMap);
  });
}

Next, call the getCatData() you just added within initState:

@override
void initState() {
  super.initState();
  getCatData();
}

Be sure to import all the class files you need at the top:

import 'dart:convert';
import '../api/cat_api.dart';
import '../models/cats.dart';

Now, modify the getCat() method as follows:

Widget getCat() {
  final mediaSize = MediaQuery.of(context).size;
  if (catList.breeds.isEmpty) {
    return Container();
  } else {
    return Center(
      child: Container(
        width: mediaSize.width,
        height: mediaSize.height,
      ),
    );
  }
}  

This will return an empty Container if the list of cat breeds is empty.

In the non-empty Container(else block), after the height argument, add the following:

// 1
decoration: BoxDecoration(
    image: DecorationImage(
// 2
    image: NetworkImage(catList.breeds[0].url), fit: BoxFit.contain,
)),

Here, you have:

  1. BoxDecoration to let you draw an image in a box area.
  2. NetworkImage to load an image from the network.

Notice how you’re using a decoration to display a network image. You don’t have to do a thing — just wrap the cat URL in a NetworkImage widget. Awesome, right? :]

Build and run your app, and tap any breed. You’ll now see cat images for each breed. Congratulations!

Cat detail iOS

Where to Go From Here?

You can download the final completed project by clicking Download materials at the top or bottom of this tutorial.

Wow, that was a lot of work, but you learned how to:

  • Use the HTTP library to issue network API requests.
  • Make API calls to return data.
  • Parse returned JSON into model classes.
  • Display lists of items in a ListView.
  • Display a network image.

You can learn more about Flutter JSON parsing by visiting JSON and serialization in the Flutter docs and the docs for the JSON Serializable package.

Feel free to share your feedback and findings or ask any questions in the comments below or in the forums. I hope you enjoyed learning about JSON parsing in Flutter!

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *