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 2 of 4 of this article. Click here to view the first page.

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:

  • 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.
  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:
  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 as BLoC 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.