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"
}
]
}
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"]}');
}
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;
}
}
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();
},
),
),
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
);
}
}
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,
}),
);
}
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
# 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
- Complete Exercise 5 Basic Flutter App
- Read all the notes for modules 12.1, 12.2, 13.1
- Start working on Hybrid step six