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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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 with FutureBuilder. As the name suggests, this widget builds itself based on the latest snapshot of interaction with a Future.
  • FutureBuilder takes an instance of Future 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, a CircularProgressIndicator during the fetching of data from the server.
  • bookListItem(), which you’ll add next, will return a ListTile widget for each book item from the collection. These ListTile widgets will be presented in a vertical stack in a ListView 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,
      ),
    );
  }
Note: You would normally never make network calls inside 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

Note: If you are following along with the tutorial using an Android emulator, you need to run this command in a Terminal window after you startup the emulator, in order to have correct port forwarding from the emulator: 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:

Your favorite books list

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:

StreamController

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 the Result 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.
  • StreamController

    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:

  1. addBook takes a Book as a parameter.
  2. client.request() makes a POST request to the endpoint, /addBook. You pass the book as an argument.
  3. _addStream.sink.add(...) adds the event to the StreamSink. 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:

  1. listen adds a subscription to the stream.
  2. LoadingState will show a progress dialog.
  3. In SuccessState, you’ll navigate back to the “Favorite Book” screen.
  4. ErrorState will show a SnackBar 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:

Updated Add Book screen

If the POST request was successful, you’ll see your book’s details at the end of the list in Favorite Book screen.

Sagar Suri

Contributors

Sagar Suri

Author

Bruno

Tech Editor

Sandra Grauschopf

Editor

Julia Zinchenko

Illustrator

Joe Howard

Final Pass Editor

Brian Kayfitz

Team Lead

Over 300 content creators. Join our team.