Flutter Apps

12.2 API Calls Continued

# Http and Multidimensional Data

When you get JSON data from an API call, it is rare that you will get one simple object like {"id":1, "title":"hello"}. Most of the results that you get will contain arrays and other two dimensional data.

The way we access those properties are very much like how we would access data in nested objects and arrays in JavaScript. Simply use the square bracket notation with integers data[0] for arrays and with property names for objects (Maps) data['property_name'].

Using this data as an example set:

{
  "timestamp": 1500123400987000,
  "results": [
    {
      "id": 123,
      "artist": "Green Day"
    },
    {
      "id": 456,
      "artist": "Pink"
    },
    {
      "id": 789,
      "artist": "Volbeat"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

This is how we would get some of the example properties.

Future<Map> getData(String keyword) async {
  Map<String, dynamic> params = {
    'apikey': 'asdkjskdfjhskhfksdjh',
    'keyword': 'music',
  }
  Uri uri = Uri.https(domain, path, params);
  http.Response response = await http.get(uri); //http get request
  Map<String, dynamic> data = jsonDecode(response.body);
  //convert the json String in the response body into a Map object with String keys.

  print( data['timestamp'] ); //1500123400987000
  print( data['results'][0]['id']);  //123
  print( data['results'][1]['artist']); //Pink
  print( data['results'][2]['artist']); //Volbeat

  //use a for loop to output all the artist names
  int len = data['results'].length;
  for (int i = 0; i < len; i++) {
    print( data['results'][i]['artist'] );
  }
  //use a forEach method call to output all the id and artist values
  data['results'].forEach( getArtist );
}

void getArtist(Map<dynamic, dynamic> artist){
  print( '${artist["id"]} ${artist["artist"]}');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Http, State and ListView

When you retrieve data with an http get or post call, this will often be used to create the items inside of a ListView. You can simply pass your data variable to the ListView.builder to create the list. However, your list may need to be updated. Maybe items will be removed. Maybe a refresh action will get a new version of your data. Whatever the reason, your ListView items may need to be updated.

To be able to update the widgets in your current screen, we will need to put your data into a State variable and then use that State variable as the data source for the ListView.builder. By doing this, it means that any time setState gets called, it will be the trigger to update the rendering of the ListView widget tree by running the build function again.

Here is the full repo for using State and Http with a ListView.builder (opens new window).

//A ListView widget built using data from a state variable.
// do a fetch call to get the data and build the list after the data has arrived.
class MainPage extends StatefulWidget {
  MainPage({Key? key, required this.title}) : super(key: key);
  final String title; //we can pass things into our widgets like this

  
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  //entries will be populated with API data
  final List<String> stuff = <String>['one text', 'two text', 'three text'];
  late List<User> users = <User>[];

  
  void initState() {
    super.initState();
    //go get the user data
    getData();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title), //widget.title gets title from MainPage
        centerTitle: true,
      ),
      body: Column(
        children: [
          Expanded(
            flex: 1,
            child: ListView.builder(
              padding: EdgeInsets.all(16.0),
              itemCount: stuff.length,
              itemBuilder: (BuildContext context, int index) {
                //gets called once per item in your List
                return ListTile(
                  title: Text(stuff[index]),
                );
              },
            ),
          ),
          Divider(
            color: Colors.black38,
          ),
          Expanded(
            flex: 2,
            //ternary operator for empty list check
            //using List.isNotEmpty rather than List.length > 0
            child: users.isNotEmpty
                ? ListView.builder(
                    padding: const EdgeInsets.all(8),
                    itemCount: users.length,
                    itemBuilder: (BuildContext context, int index) {
                      //gets called once per item in your List
                      return ListTile(
                        leading: CircleAvatar(
                          backgroundColor: Colors.amber,
                          child: Text('A'),
                        ),
                        title: Text('${users[index].name}'),
                        subtitle: Text(users[index].email),
                      );
                    },
                  )
                : ListTile(
                    title: const Text('No Items'),
                  ),
          ),
        ],
      ),
    );
  }

  //function to fetch data
  Future getData() async {
    print('Getting data.');
    HttpHelper helper = HttpHelper();
    List<User> result = await helper.getUsers();
    setState(() {
      users = result;
      print('Got ${users.length} users.');
    });
  }

  //function to build a ListTile
  Widget userRow(User user) {
    Widget row = ListTile(
      title: Text(user.name),
      subtitle: Text(user.email),
    );
    return row;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

The example above uses a state variable late List<User> users = <User>[]; that holds a default value of an empty list of User objects. The User object is defined in /lib/data/user.dart as a class that has an instance variable for each property we want a user to have. The class declares the variables, has a standard constructor and a custom constructor that will build a User object from a Map<String, dynamic> userMap being passed from from the fetched JSON data.

There is also an HttpHelper class in /lib/data/http_helper.dart. It does the actual fetch call to the API, accepts the JSON string, converts the JSON string into a List<dynamic>, and then loops through the list and calls on the User class to convert the elements in the List from Map<String, dynamic> to User<String, dynamic>.

Inside the 2nd Expanded(child:) note the use of the ternary operator as the value.

# Cookbook version without state

The cookbook recipe for fetching data (opens new window) shows how to add a FutureBuilder as a widget in the widget tree. Let's say that we have an Expanded element that contains a ListView. The ListView will eventually hold the results of an http.get command.

Expanded(
  child: FutureBuilder<Stuff>(
    future: futureStuff,
    builder: (context, snapshot) {
      if (snapshot.hasData) {
        return MyListView(snapshot.data); //custom ListView.builder widget to show the data from snapshot
      } else if (snapshot.hasError) {
        return Text('${snapshot.error}');
      }

      // By default, show a loading spinner.
      return const CircularProgressIndicator();
    },
  ),
),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

The future property is given a function futureStuff that will do the fetching of the data.

Future<Stuff> fetchAlbum() async {
  final response = await http.get(Uri.parse('https://example.com/endpoint'));
  if (response.statusCode == 200) {
    return Stuff.fromJson(jsonDecode(response.body));
  }else{
    throw Exception('Failed to load album'); //populates the snapshot.hasError and snapshot.error properties
  }
}

//our custom data object
class Stuff {
  //declare variables
  //create default constructor
  Stuff({
    //default props
  });
  //create custom constructor for from json
  Stuff.fromJSON(Map<String, dynamic> json){
    return Stuff(
      //with props
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

This way we are effectively doing the same thing as we do with our state variable List in the first example.

Here is the reference for CircularProgressIndicator (opens new window)

Here is the reference for a FutureBuilder (opens new window).

Here is the reference for the AsyncSnapshot class (opens new window).

# Notes on Performance

When you are fetching data from an API and the API returns a large JSON file, it can sometimes take a while to actually parse the JSON string and convert it into a Map or List. If it takes longer than 16ms (one frame) then the user may experience what is called Jank. Jank is visual interruptions in the interface. Maybe an animation stops and starts, maybe a button is unresponsive, maybe a transition stalls...

To avoid problems like this, we can move the actual computation of the conversion from JSON to Map into a new thread by wrapping our function call inside a compute() method call.

Here is the guide to using compute with JSON conversion (opens new window).

# Uploading Data

When making a call to an API that needs headers or data to be uploaded that is not in the querystring, we can add the optional parameters in our calls to http.get or http.post, etc.

Future<http.Response> createAlbum(String title) {
  return http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/albums'),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(<String, String>{
      'title': title,
    }),
  );
}
1
2
3
4
5
6
7
8
9
10
11

The default first argument for post or get is the Uri. After that come the optional named parameters - headers and body. The headers is a Map<String, String> and the body is a Map<String, dynamic> that will be converted into a JSON string with a call to jsonEncode().

When working with JWT you will need to pass the token string inside a header.

Most API calls to post or put or patch will require a body value.

# Sample Repo

Here is a Github repo with demonstrations of uploading data and error handling Http requests (opens new window).

# Work on Exercise 5

Exercise 5 Flutter App is due this week. Let's take time in class to actually complete the exercise. Use the flutter-nav-starter repo that we used in class in week 11 as your reference to build your own Flutter App.

# What to do this week

TODO

Things to do before next week

Last Updated: 4/11/2022, 4:45:36 PM