Flutter Apps

12.2 API Calls Continued

Module Still Under Development

# 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.

//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() {
    //Think of the initState like react useEffect() for initial load
    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
97

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

circularProgressIndicator Widget of the Week

# 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.

# 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, inside the headers property.

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

# Loading and Accessing Bundle Assets

In the pubspec.yaml file under the flutter: assets: section we usually load a list of image files that we want to be able to load through AssetImage() or Image.asset(). However, we can also load properties, settings, and string values that we want to use in our app by including a JSON file in that list.

If you want to access the files loaded through the pubspec.yaml file we can do it through a couple ways. There is a property called rootBundle which will access those files. The rootBundle property is located in the package:flutter/services.dart package.

This code sample shows how you can access a JSON file called config.json inside a folder called /assets/.

import 'package:flutter/services.dart' show rootBundle;

Future<String> loadAsset() async {
  return await rootBundle.loadString('assets/config.json');
}

void main() {
  loadAsset().then((value) {
    Map map = json.decode(value);
    map['key2'] = 'value2';
    print(map);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13

Then you can use the jsonDecode() method, imported from dart:convert, to read the contents of the file.

The rootBundle represents all the files under that assets heading in the pubspec. Another way to access the JSON data file is with DefaultAssetBundle class and it's of method. It uses the current context to access the value of the bundle as it has been passed down through the element tree.

import 'package:flutter/services.dart';

Future<String> loadAsset(BuildContext context) async {
  return await DefaultAssetBundle.of(context).loadString('assets/config.json');
}
1
2
3
4
5

The reason you would use this second approach would be that you would want to include any updates to the bundle that have been made further up the element tree.

There is a base abstract class that is used for handling the assets, called AssetBundle. rootBundle is an implementation of the AssetBundle class, and so is the DefaultAssetBundle hook to the rootBundle. Additionally, there are two other classes that implement AssetBundle - NetworkAssetBundle and CachingAssetBundle.

The NetworkAssetBundle class and the CachingAssetBundle class both have load() and loadString() methods (plus a few others) which can load files. The primary difference is that the caching version's loadString method will save the loaded data in a cache to be kept for the lifetime of the app. Both methods also have a clear() method to clear everything cached or loaded, plus a evict(String key) method to remove a single peice of loaded data.

# What to do this week

TODO Things to do before next week

  • Complete Flutter App 2
  • Read all the notes for modules 12.1, 12.2, 13.1
Last Updated: 11/19/2023, 7:43:24 PM