Welcome to another read about the Flutter and Dart world. Here we are going to learn to use Flutter and Dart to efficiently make HTTP requests to send/receive data from servers. Most apps nowadays require an Internet connection to fetch data from a server, authenticate a user, or simply download content. Without further introduction, let’s dive into the http and the dio packages.
In this example we’re connecting to JSONPlaceholder, an online API used for testing purposes. It’s a free service at which you can send GET or POST requests and it returns various types of JSON-encoded strings as a response.
dependencies:
http: <latest-version>
Our code has to be easily maintainable over time and easy to read: dependency injection (DI) and the single responsibility principle (SRP) could help us for sure! We’re going to create an interface to be implemented by classes that want to perform any kind of HTTP request.
1
2
3
abstract class HTTPRequest < T > {
Future < T > execute();
}
The online API we’re connecting to returns a JSON string containing data about a test item. The next step is implementing HTTPRequest<T>
to perform a GET request and returning a model class Item representing the received data.
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
// item.dart
class Item {
final int id;
final String title;
const Item({
required this.id,
required this.title,
});
factory Item.fromJson(Map < String, dynamic > json) {
return Item(
id: json['id'] as int,
title: json['title'] as String,
);
}
}
// request_item.dart
class RequestItem implements HTTPRequest < Item > {
final String url;
const RequestItem({
required this.url,
});
Future < Item > execute() async {
final response = await http.get(url);
if (response.statusCode != 200) {
throw http.ClientException("Oh darn!");
}
return _parseJson(response.body);
}
Item _parseJson(String response) {
final data = jsonDecode(response);
return Item.fromJson(data);
}
}
The http package performs asynchronous POST or GET requests and returns a response object which exposes many useful properties:
body: the response body (it’s a string);
statusCode: the HTTP status code (it could be 200, 404 or 500, for example);
contentLength: the size of the response;
headers: the headers the server sent to our request.
Let’s now move to the Flutter side. When dealing with futures, like in the case of HTTP requests, we need to use stateful widgets to cache the Future<T>
object. It has to be instantiated only once otherwise the FutureBuilder will reload the future multiple times (causing undesired behaviors).
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
class HTTPWidget extends StatefulWidget {
final HTTPRequest < Item > request;
const HTTPWidget({
required this.request,
});
@override
_HTTPWidgetState createState() => _HTTPWidgetState();
}
class _HTTPWidgetState extends State < HTTPWidget > {
late final futureItem = widget.request.execute();
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureItems,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const SomeErrorWidget();
}
if (snapshot.hasData) {
return SomeSuccessWidget(
data: snapshot.data,
);
}
return const Center(
child: CircularProgressIndicator()
);
}
);
}
}
Thanks to the late final, the future is lazily initialized only once so the API calls triggered by execute()
are also executed one time only. It would have been absolutely WRONG if we fetched the HTTP data directly inside the build method:
1
2
3
4
5
6
7
8
9
10
11
12
13
class _HTTPWidgetState extends State < HTTPWidget > {
@override
Widget build(BuildContext context) {
final futureItem = widget.request.execute();
return FutureBuilder(
future: futureItems,
builder: (context, snapshot) {
// code...
}
);
}
}
Flutter could (potentially) call the build method hundreds of times, so, with the above code, you’re making an HTTP request at every widget rebuild. Other than negatively affecting performance, it’s also undesired because you probably don’t want to “waste” Internet connection, battery, and not persisting data. If you don’t like the late final approach you can go this way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class _HTTPWidgetState extends State < HTTPWidget > {
final Future < Item > futureItem;
@override
void initState() {
super.initState();
futureItems = widget.request.execute();
}
@override
Widget build(BuildContext context) {
// code...
}
}
There’s no difference here between using a late final variable and initState, it’s just a matter of taste! Now that we have learned how to “cache” the request, it’s time to look at the FutureBuilder<T>
widget that simply notifies the UI whenever a future has been completed with success or error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
FutureBuilder( future: futureItems, builder: (context, snapshot) { if (snapshot.hasError) { return const SomeErrorWidget(); } if (snapshot.hasData) { return SomeSuccessWidget( data: snapshot.data, ); } return const Center( child: CircularProgressIndicator() ); } );
The AsyncSnapshot<T>
snapshot variable gives you information about the status of the future:
hasError: this property evaluates to true whenever an exception occurs in the future. A proper widget appears on the screen indicating that something has gone wrong.
hasData: this is false by default but it becomes true as soon as the future completes with success. In other words, this is a “flag” we can use to determine whether data are ready to appear on the UI or not.
The last return statement places the loading spinner at the center of the screen. It’s a default fallback option that will either be replaced by the error or success widget.
Executing POST requests is no different from GET: you just have to provide an additional body parameter which is the payload to send to the server.
1 2 3 4
final response = await http.post( 'https://myendpoint.com/api/v1/send', body: "send this string via POST", );
There’s the possibility to specify different encodings using encoding: Encoding.getByName("utf-8")
. The body of the request can be of three different kinds:
string: the content-type of the request is automatically set to text/plain;
List<T>
: the list of bytes of the body of the request;
Map<K, V>
: it’s treated as if it contained form fields and it automatically sets the content type to application/x-www-form-urlencoded
When you do a POST request, the returned object is a Future<Response>
. For both get()
and post()
you have the possibility to set headers for the HTTP request; they’re simply implemented as a map where both keys and values are strings:
1 2 3 4 5 6 7
final response = await http.post( 'https://myendpoint.com/api/v1/send', body: "send this string via POST", headers: { 'Authorization': 'key', } );
The response object, which is what the request returns, contains the headers returned by the server. It’s a Map<String>
, String so you can very easily access it using the [] operator.
The http package can also perform I/O operations but dealing with them is not so easy. You’d have to work with bytes, encodings, file objects, and other things a bit too “low level”. There is a very powerful package called dio that easily works with file download/upload and, of course, many other networking duties such as HTTP requests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final dio = Dio();
final url = 'https://website.com';
try {
// a GET request
final response1 = await dio.get < String > (url);
// a POST request
final response2 = await dio.post < String > (url, data: {
'a': '',
'b': 1,
});
}
on DioError
catch (e) {
...
}
The code is very easy to read and also very intuitive. If you’re only going to perform GET or POST requests, it’s not a big deal, any library is good. When it comes to handling file download and upload instead, (or any other I/O action), dio is way easier to use than http. Here’s how you can download a file from a server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
final dio = Dio();
await dio.download(
'https://website.com/file-to-download.txt',
'myFiles/demo.txt',
onReceiveProgress: (received, total) {
// If the 'content-length' header is not sent from the server, the value
// of 'total' will always be set to -1 so we want to make sure that the
// progress percentage can be computed.
if (total != -1) {
...
}
}
);
Very clean and easy! We have just passed in the URL of the file to download and the location in our device in which the file will be saved. The onReceiveProgress parameter is optional but it’s really useful when we want to show the progress percentage of the download. Please keep in mind the following:
There is no download method in the http package so you’d have to craft the function yourself.
There is no way to directly calculate the progress percentage of a download in http: you’d have to extract the bytes and make the calculations yourself.
As you can see, dio is very convenient. For sake of completeness, let’s also see how we can upload files to a server using dio. Let’s pretend we wanted to fill this HTML form using a Flutter (or Dart) application:
1 2 3 4 5
<form method="post" action="/admin/new-user" enctype="multipart/form-data"> <input type="text" name="nickname" /> <input type="file" name="avatar" accept="image/jpeg" /> <input type="submit" value="Upload" /> </form>
If we were in a browser, we would fill the forms and press the “upload” button. With dio, we can do these same actions simply by using the upload method. You may already be familiar with this code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final dio = Dio();
// The image to be uploaded
final imagePath = 'path/to/image.jpg';
// Filling the HTML form programmatically
final payload = FormData.fromMap({
'nickname': 'Roberto',
'file': await MultipartFile.fromFile(imagePath),
});
// Pressing the "Upload" button
await dio.post < String > (
'/admin/adduser',
data: payload,
onSendProgress: (sent, total) {
if (total != -1) {
...
}
}
);
Thanks to the onSendProgress callback, we can keep track of the upload progress. With FormData the default encoding is 'multipart/form-data'
so our request can also send files. Please note that you can upload as many files as you want:
1 2 3 4 5 6 7 8
final payload = FormData.fromMap({ 'nickname': 'Roberto', 'files_list': [ await MultipartFile.fromFile('path/to/image/1.jpg'), await MultipartFile.fromFile('path/to/image/2.jpg'), await MultipartFile.fromFile('path/to/image/3.jpg'), ], });
I want to point out that dio is not better than http but in certain cases it’s easier to use because it has a bigger API. Both packages are great and very performant but in certain situations you might prefer one or the other. In general, both are fine in most cases but dio shines when you want to handle files IO or easily handle retry policies.
In the next article we will see how you can publish new packages at pub.dev. A very big slice of the Dart and Flutter world is maintained by the community because thousands of developers contribute to the growth of the package ecosystem by publishing their work. If you want to discover more about this make sure to read the next article!
The official Dart Tour guide
CHECK OUT TOPCODER FLUTTER FREELANCE GIGS