Flutter Networking Tutorial: Getting Started
In this tutorial, you’ll learn how to make asynchronous network requests and handle the responses in a Flutter app connected to a REST API. By Sagar Suri.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Flutter Networking Tutorial: Getting Started
25 mins
- Getting Started
- Some Important Terminology
- What is a Network Request?
- What is a RESTful API?
- HTTP Methods
- Exploring the Project
- Deploying REST Apps
- Making GET Requests
- Creating Data Model Classes
- Understanding the HTTP Client
- Implementing Network Logic
- Building the ListView
- Displaying the Favorite Books List
- Making a POST Request
- Updating the Add Book Screen
- Making a DELETE Request
- Where to Go From Here?
Building the ListView
In the previous section, you implemented the network logic to make a GET request and fetch the list of books. Now, you’ll display those books in a ListView
.
Navigate to lib/ui/favorite_book_screen and open favorite_book_screen.dart.
Now, to show the result of the network response, you need to call getBooks()
from within FavoriteBooksScreen
and wait for the result.
In order to access the getBooks()
method you need to create an instance of RemoteDataSource
inside _FavoriteBooksScreenState
. Replace the first TODO with the following code:
RemoteDataSource _apiResponse = RemoteDataSource();
Next, you need to fetch the list of your favorite books from the backend and display them in a list. To perform this sort of operation Flutter provides a very handy widget named FutureBuilder
. You can use that widget to get the task done. Update the second TODO by replaing the current child
with the following code:
child: FutureBuilder(
future: _apiResponse.getBooks(),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
Library bookCollection = (snapshot.data as SuccessState).value;
return ListView.builder(
itemCount: bookCollection.books.length,
itemBuilder: (context, index) {
return bookListItem(index, bookCollection, context);
});
} else if (snapshot.data is ErrorState) {
String errorMessage = (snapshot.data as ErrorState).msg;
return Text(errorMessage);
} else {
return CircularProgressIndicator();
}
}),
)
Looking over this code, you see that:
- You’ve replaced
Text
withFutureBuilder
. As the name suggests, this widget builds itself based on the latest snapshot of interaction with aFuture
. -
FutureBuilder
takes an instance ofFuture
to which it is currently connected. -
AsyncSnapshot
holds the result of the HTTP response. Based on the snapshot’s data, it provides an appropriate widget. For example, aCircularProgressIndicator
during the fetching of data from the server. -
bookListItem()
, which you’ll add next, will return aListTile
widget for each book item from the collection. TheseListTile
widgets will be presented in a vertical stack in aListView
widget.
Now implement bookListItem()
to return a ListTile
widget containing the details of a book from the collection of favorite books. Add the following code at the bottom of the _FavoriteBooksScreenState
class:
ListTile bookListItem(
int index, Library bookCollection, BuildContext context) {
return ListTile(
leading: Image.asset("images/book.png"),
title: Text(bookCollection.books[index].name),
subtitle: Text(
bookCollection.books[index].description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.caption,
),
isThreeLine: true,
trailing: Text(
bookCollection.books[index].author,
style: Theme.of(context).textTheme.caption,
),
);
}
build
, because build
could get called every frame. Making 60 calls every second is a worst-case scenario. But for the simplicity of the tutorial, you’re going to break that rule and make the call in build
.Breaking down the above implementation:
- The method returns a
ListTile
widget which will hold the details of the book item. - A
ListTile
contains one to three lines of text optionally flanked by icons.
Displaying the Favorite Books List
adb reverse tcp:8888 tcp:8888
. The adb
version you use must be the one inside your local Android SDK installation, which is often in ~/Android/Sdk/platform-tools. The output from the command should be the port 8888. Build and run the app and see what output you get. If the backend REST app is up and running, you should see the following output:
Making a POST Request
Next, you’ll add the network logic to upload the details of your favorite book by sending the data through a POST request. Go back to lib/network/remote_data_source.dart. In the third TODO
, you create a StreamController object:
StreamController<Result> _addBookStream;
Here’s what you’ll be using the StreamController for:
The fourth TODO
initializes the StreamController
. Add the following code inside init()
:
-
StreamController
allows sending data, error and done events on its stream. You’ll use it to send theResult
to the UI and to update it accordingly. -
StreamController
has two important getters: sink and stream. The sink is of type StreamSink which has a method named add that passes events to the sink. You use stream to get the event that was added to the sink.
The fourth TODO
initializes the StreamController
. Add the following code inside init()
:
_addBookStream = StreamController();
Now, you’ll add the logic to make a POST request. Replace the fifth TODO
with the following method:
//1
void addBook(Book book) async {
_addBookStream.sink.add(Result<String>.loading("Loading"));
try {
//2
final response = await client.request(
requestType: RequestType.POST, path: "addBook", parameter: book);
if (response.statusCode == 200) {
//3
_addBookStream.sink.add(Result<NetworkResponse>.success(
NetworkResponse.fromRawJson(response.body)));
} else {
_addBookStream.sink.add(Result.error("Something went wrong"));
}
} catch (error) {
_addBookStream.sink.add(Result.error("Something went wrong!"));
}
}
Breaking down this code:
-
addBook
takes aBook
as a parameter. -
client.request()
makes a POST request to the endpoint,/addBook
. You pass thebook
as an argument. -
_addStream.sink.add(...)
adds the event to theStreamSink
. Now,stream
can provide these events to the UI and update it accordingly.
Next, you’ll create a getter method in RemoteDataSource
that returns the stream
of the StreamController
so that the user can see it in the UI. To do this, replace the sixth TODO
with the following code:
Stream<Result> hasBookAdded() => _addBookStream.stream;
Since you opened a stream to add events, you must close the stream when you’re done observing the changes. Otherwise, you’ll get unwanted memory leaks.
In dispose()
, replace the seventh TODO
with the following code:
_addBookStream.close();
Updating the Add Book Screen
Navigate to lib/ui/addscreen and open add_book_screen.dart. The first TODO
is to create the RemoteDataSource
object. Replace the first TODO
with the following code:
RemoteDataSource _apiResponse = RemoteDataSource();
You need to initialize the remote data source in initState()
of _AddBookScreenState
. Update the second TODO
using the following code:
@override
void initState() {
super.initState();
_apiResponse.init();
}
In this code:
-
initState()
is a method which is called once when the stateful widget is inserted in the widget tree. - You call
initState()
when you add AddBookScreen to the widget tree. You’ll call this method only once, when AddBookScreen is first created.
Next, you have to listen to the stream
, exposed by the RemoteDataSource
object, for the Result
that will be delivered through the sink
after the POST request completes.
To do this, replace the third TODO
with the following code:
void hasBookAddedListener() {
//1
_apiResponse.hasBookAdded().listen((Result result) {
//2
if (result is LoadingState) {
showProgressDialog();
//3
} else if (result is SuccessState) {
Navigator.pop(context);
Navigator.pop(context);
//4
} else {
SnackBar(
content: Text("Unable to add book"),
duration: Duration(seconds: 2),
);
}
});
}
Breaking down the code:
-
listen
adds a subscription to the stream. -
LoadingState
will show a progress dialog. - In
SuccessState
, you’ll navigate back to the “Favorite Book” screen. -
ErrorState
will show aSnackBar
with the error message.
Update initState
to call the method you just added:
@override
void initState() {
super.initState();
_apiResponse.init();
hasBookAddedListener();
}
Finally, you’ll add the logic to submit the book’s detail and make a POST request to upload the details that the user enters.
Replace the fourth TODO
with the following code:
final book = Book(
name: _name, author: _author, description: _description);
_apiResponse.addBook(book);
That will collect the details of your book from the TextField
and make a POST request.
Congrats! Build and run the app, click the add button, and try adding your favorite book’s details:
If the POST request was successful, you’ll see your book’s details at the end of the list in Favorite Book screen.