5

Flutter Networking Tutorial: Getting Started [FREE]

 4 years ago
source link: https://www.raywenderlich.com/5896601-flutter-networking-tutorial-getting-started
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In today’s world, smartphones have become the primary source of entertainment, banking, photo/videography and shopping. To do many of the things their users request, from ordering food to booking movie tickets, apps on your smartphone needs Internet access.

If you plan to develop apps that fetch data from the Internet, you’ll need to know how to make network requests and how to handle the responses properly. Throughout this tutorial, you’ll learn how to do just that by building a Flutter app.

Getting Started

You’ll build an app named Favorite Books that will download a list of popular books including details like title , description and author’s name and display it in a ListView . You can add your favorite book to the existing list and the details will upload to the server. You can even delete a book from the list.

While building this app, you’ll learn the following things:

  • Locally deploying a RESTful API using the Aqueduct framework.
  • Making GET , POST and DELETE requests.
  • Using an HTTP client to make the network request.
  • Understanding Future , async and await .
  • Handling different states like Loading , Success and Error .

Note : This tutorial assumes prior knowledge of Dart and the Flutter framework for developing cross-platform mobile apps. If you are unfamiliar with Flutter, please see Getting Started with Flutter .

To begin, download the starter project using the Download Materials button at the top or bottom of this tutorial. Open it in Visual Studio Code, Android Studio or your favorite editor and build the project. Run it to see the current state of the app:

Simulator-Screen-Shot-iPhone-X%CA%80-2019-10-11-at-18.29.51-231x500.png

Simulator-Screen-Shot-iPhone-X%CA%80-2019-10-11-at-18.29.57-148x320.png

Note : If you have any issues building the app, enter the command line flutter packages get in a terminal in the project root directory. This will fetch any missing dependencies.

Some Important Terminology

Before you start coding, take a moment to be sure that you understand the terminology that you’ll see throughout this tutorial.

What is a Network Request?

In simple terms, when you open an app like Whatsapp, Instagram or Twitter on your smartphone, the app tries to fetch the latest data from a remote location, usually called a Server . It then displays that information to the user. A server is a centralized place that stores most user data. The app that you’re using is called the Client .

Screenshot-2019-10-08-at-2.35.11-PM-220x320.png

So a network request is a request for data from a client to a server.

What is a RESTful API?

REST stands for REpresentational State Transfer. It’s an application program interface (API) that uses HTTP requests to get or send data between computers.

Communication between a client and a server mostly happens through RESTful APIs.

The most basic form of a REST API is a URL that the client uses to make a request to the server. Upon receiving a successful request, the server checks the endpoint of the URL, does some processing and sends the requested data or resource back to the client.

HTTP Methods

There are four main HTTP methods that you use in REST APIs. Here’s what each of them does:

  • GET : Requests a representation of the specified resource. Requests using GET only retrieve data – they should have no other effect on the data.
  • POST : Submits data to the specified resource. You use this method to send data to the server, such as customer information or file uploads.
  • DELETE : Deletes the specified resource.
  • PUT : Replaces all current representations of the target resource with the uploaded content.

Now that you have some theory under your belt, you can move on to exploring the starter project.

Exploring the Project

Once you’ve run the starter project, it’s time to take a look at the project structure. Start by expanding the lib folder and checking the folders within it. It should look like this:

Captura-de-Tela-2019-10-28-%C3%A0s-22.26.42.png

Here’s what each package inside the lib directory does:

  • model : This package consists of data model classes for each type of network response.
  • network : This package holds the networking logic of the app.
  • ui : Contains different UI screens for the app.

Open pubspec.yaml in the project root, and notice that there is an http package added under dependencies . You will be using the http package created by the official dart team to make HTTP requests:

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  http: ^0.12.0+3

Deploying REST Apps

Inside the downloadable material’s folder, you’ll find a file named book_api_app.zip . That project serves as the backend/server for your Flutter app for this tutorial. It will handle all the requests coming from your app and provide you with the appropriate response.

The backend project uses the Aqueduct framework, which is an HTTP web server framework for building REST applications written in Dart. You won’t be getting into how to write server-side code using Dart; you will just deploy this project and focus on implementing the networking logic on the frontend, i.e the Flutter app, to communicate with the backend.

To start making HTTP requests to the book_api_app , you need to deploy the app locally on your machine.

Follow the steps to deploy an Aqueduct app locally:

  1. Unzip the book_api_app.zip file.
  2. Type this command in your terminal from the root of the book_api_app project: flutter pub get .
  3. Type this command in your terminal from the root of the book_api_app project: flutter pub global activate aqueduct .
  4. The previous step will give you a warning about your path, which you can fix with a command like export PATH="$PATH":"$HOME/bin/flutter/.pub-cache/bin" . Run the export command you are given by the previous step. The specific path will be a subdirectory of where your flutter install is located.
  5. Now, execute the command: aqueduct serve in the project folder.

You should see something like the following in your terminal:

Screenshot-2020-01-12-at-10.08.07-PM.png

The base URL for this app after locally deploying the application is http://localhost:8888 .

Making GET Requests

The simplest and easiest way to make a GET request is to load the URL with the endpoint as /books on a browser.

Copy http://localhost:8888/books and enter it into your favorite browser. You should see the following output in your browser’s page:

Screenshot-2020-01-12-at-10.02.41-PM-650x420.png

The above image shows the JSON body of a successful GET request. The response body is a list of favorite books. Depending on your browser settings, the JSON may not be formatted quite as nicely as the screenshot.

Now, go back to your terminal. You should see a GET request near the bottom:

Screenshot-2020-01-12-at-10.11.01-PM.png

Whenever a request is made to the book_api_app app, the app prints out the status code of the response in the terminal.

Next, you’ll make the same GET request, but this time you’ll add the request logic in the app and display it in a Flutter ListView .

Creating Data Model Classes

As you can see, the response from the server is in the form of JSON, which Dart cannot interpret directly. So you have to convert the JSON response to something that Dart can understand, i.e parse the JSON and store the details in Dart objects. Back in the starter project, navigate to libs/model/library.dart . You’ll create a data model class that will hold the logic to convert the JSON response to a custom Dart object.

Looking at the JSON response you can see it consists of an array of JSON elements that holds a number of JSON objects. Each JSON object consist of a book detail.

Inside libs/model/library.dart , create a class Book which will hold the details of a book:

class Book {
  final String name;
  final String author;
  final String description;

  Book({this.name, this.author, this.description});

  factory Book.fromJson(Map<String, dynamic> json) => Book(
      name: json["name"],
      author: json["author"],
      description: json["description"]);

  Map<String, dynamic> toJson() =>
      {"name": name, "author": author, "description": description};
}

A Book holds the properties of a book and provides a custom Dart object. Note that factory constructors are used to prepare calculated values to forward them as parameters to a normal constructor so that final fields can be initialized with them.

Now in the same file create a class Library that will convert the JSON to a list of Book ‘s object:

class Library {
  final List<Book> books; // 1


  Library({this.books});

  factory Library.fromRawJson(String str) =>
      Library.fromJson(json.decode(str)); // 2

  factory Library.fromJson(Map<String, dynamic> json) => Library(
      books: List<Book>.from(
          json["bookList"].map((x) => Book.fromJson(x))));

  Map<String, dynamic> toJson() => {
        "bookList": List<dynamic>.from(books.map((x) => x.toJson())),
      };
}

Add a necessary import to the top of the file:

import 'dart:convert';

In the Library code:

  1. Library holds a list of Book objects.
  2. json.decode(jsonString) converts the JSON string to a Map object. The key in the Map object will hold the key of the JSON object, and its value will hold the value of that particular key.

When you make a POST request to add the details of your favorite book, you’ll get the following response:

{
   "message": "Book details have been added successfully"
}

You need to parse the JSON and convert it to custom Dart object similar to what you did in the previous step.

Navigate to lib/model and open network_reponse.dart . Create a class NetworkResponse which will parse the JSON and hold the message sent from the server:

import 'dart:convert';

class NetworkResponse {
  final String message;

  NetworkResponse({this.message});

  factory NetworkResponse.fromRawJson(String str) =>
      NetworkResponse.fromJson(json.decode(str));

  factory NetworkResponse.fromJson(Map<String, dynamic> json) =>
      NetworkResponse(message: json["message"]);

  Map<String, dynamic> toJson() => {"message": message};
}

Next, you need to understand what an HTTP client is and the importance of using one in the app.

Understanding the HTTP Client

To be able to send a request from your app and get a response from the server in HTTP format, you use an HTTP client. Navigate to lib/network and open book_client.dart . BookClient is a custom client created specifically to make requests to your book_api backend.

class BookClient {
  // 1
  static const String _baseUrl = "http://127.0.0.1:8888";
  // 2
  final Client _client;
  BookClient(this._client);
  // 3
  Future<Response> request({@required RequestType requestType, @required String path, dynamic parameter = Nothing}) async {_
​    // 4
​    switch (requestType) {
​      case RequestType.GET:
​        return _client.get("$_baseUrl/$path");
​      case RequestType.POST:
​        return _client.post("$_baseUrl/$path",
​            headers: {"Content-Type": "application/json"}, body: json.encode(parameter));
​      case RequestType.DELETE:
​        return _client.delete("$_baseUrl/$path");
​      default:
​        return throw RequestTypeNotFoundException("The HTTP request method is not found");
​    }
  }
}

In the above implementation:

  1. All the requests made through this client will go to the baseUrl , i.e. the book_api’s url.
  2. BookClient uses the http.dart Client internally. A Client is an interface for HTTP clients that takes care of maintaining persistent connections across multiple requests to the same server.
  3. The request() method takes the following parameters:
    • RequestType is an enum class holding the different type of HTTP methods available.
    • path is the endpoint to which the request has to be made.
    • parameter holds the additional information to make a successful HTTP request. For example: body for a POST request.
  4. The request() method executes the proper HTTP request based on the requestType specified.

You could customize the request() method by adding more HTTP methods in the switch statement, e.g. PUT or PATCH , based on your requirements.

Next, you’ll implement the network logic to make the GET request and parse the JSON response into Library .

Implementing Network Logic

Navigate to lib/network and open remote_data_source.dart . This class will hold the logic to make calls to different endpoints like addBook or deleteBook and return the result to the upper layer, which can be a view or a repository layer. This type of segregation of data sources into separate classes is part of a layered architecture such asBLoC or Redux.

You have to create an HTTP client that’s responsible for making network requests to a server. Replace the first TODO with the following line of code:

BookClient client = BookClient(Client());

Note : You’ll see a red line under the word Client and BookClient . Select each one of them and hit option + return on macOS or Alt+Enter on a PC. Select the Import Library option from the dropdown menu.

Breaking down the above code:

  • BookClient abstracts the implementation of HTTP requests from the RemoteDataSource since it’s only responsibility is to hold the business logic.
  • Client() creates an IOClient if dart:io is available and a BrowserClient if dart:html is available, otherwise it will throw an unsupported error.

Replace the second TODO with the following code:

//1
Future<Result> getBooks() async {
    try {
      //2
      final response = await client.request(requestType: RequestType.GET,
          path: "books");
      if (response.statusCode == 200) {
        //3
        return Result<Library>.success(Library.fromRawJson(response.body));
      } else {
        return Result.error("Book list not available");
      }
    } catch (error) {
      return Result.error("Something went wrong!");
    }
  }

Use the same hot-key as above for your platform to import any needed files.

Breaking down the code above:

  1. The return type of the method is Future . A future represents the result of an asynchronous operation, and can have two states: completed or uncompleted.
  2. client.request(requestType: RequestType.GET, path: "books") will make a GET request to the /books endpoint with an asynchronous call using the keyword await .
  3. Result is a generic class which has three subclasses: LoadingState , SuccessState and ErrorState .

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:

ListTile
ListTile

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:

Simulator-Screen-Shot-iPhone-X%CA%80-2019-10-15-at-00.44.32-231x500.png

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

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:

Simulator-Screen-Shot-iPhone-X%CA%80-2019-10-15-at-02.02.46-231x500.png

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

Making a DELETE Request

Now, you’ll add a feature to delete a particular book detail from the favorite list.

Open RemoteDataSource and replace the eighth TODO with the following code snippet:

//1  
Future<Result> deleteBook(int index) async {
  try {
    //2
    final response = await client.request(
        requestType: RequestType.DELETE, path: "deleteBook/$index");
    if (response.statusCode == 200) {
      return Result<NetworkResponse>.success(
          NetworkResponse.fromRawJson(response.body));
    } else {
      return Result<NetworkResponse>.error(
          NetworkResponse.fromRawJson(response.body));
    }
  } catch (error) {
    return Result.error("Something went wrong!");
  }
}

Here’s what this code does:

  1. deleteBook() takes the position of the book object in the list that the user wants to delete. The return type of this method is Future<Result> .
  2. client.request(...) performs the DELETE request.

Next, add the swipe to delete feature in the Favorite Book screen.

Open favorite_book_screen.dart and replace the previous bookListItem code with the following:

//1
Dismissible bookListItem(
      int index, Library bookCollection, BuildContext context) {
    return Dismissible(
      //2
      onDismissed: (direction) async {
        Result result = await _apiResponse.deleteBook(index);
        if (result is SuccessState) {
          //3
          setState(() {
            bookCollection.books.removeAt(index);
          });
        }
      },
      background: Container(
        color: Colors.red,
      ),
      key: Key(bookCollection.books[index].name),
      child: 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,
        ),
      ),
    );
  }

Breaking down the new code:

  1. You’ve wrapped the ListTitle with a Dismissible widget.
  2. In onDismissed() , you pass the index of the book item you want to delete from the backend to deleteBook() .
  3. If the DELETE request is successful, calling setState() removes the item from the list of books.

Build and run the app and try swiping on one of the book items in the list. If the DELETE request is successful, you’ll see the item removed from the list.

Where to Go From Here?

Check out the final completed project by clicking the Download Materials button at the top or bottom of the tutorial.

To learn more about Flutter networking, take a look at the official documentation:

You can also explore more about the following topics:

We hope you enjoyed this tutorial on how to make network requests in the Flutter app! Feel free to share your feedback, findings or ask any questions in the comments below or in the forums.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK