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 Karol Wrótniak.

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.

Understanding JSON

The data sent over the internet is a set of bytes or characters. Assume that you want to send the title and the author of some book. There is no information on which position each property starts and ends. You have to send some metadata to be able to interpret the response. That is where you can use JSON (JavaScript Object Notation).
It’s a language-independent data format readable by humans. It looks like this:

{
  "author":"John Doe",
  "title":"Mike \"The cursed\"",
  "rating":4.5,
  "tags":[
    "history",
    "thriller"
  ]
}

In the snippet above you have a list of key-value pairs (also called fields). For example, the author key points to John Doe. It’s the same kind of structure as Map in Dart.

The tags field is an ordered list of values, just like a List in Dart. JSON supports the following types of fields:

  1. string – enclosed in double quotes,
  2. number – can hold an int or double type,
  3. null,
  4. boolean – true or false,
  5. nested object containing any of the types from the previous list,
  6. array of elements of any type from this list.

Notice the standardized notation. There are double quotes around all the strings and the square brackets enclosing an array. Line breaks and indentation are only for human readability purposes. In the real data transferred over the internet servers usually omit them to reduce data size.

That’s only the tip of an iceberg. The JSON specification covers other topics you may want to read about, learn more in the official documentation.

Discovering Retrofit

Retrofit is a type-safe dio client generator. You can use it to generate code that makes requests to various API endpoints through HTTP methods using dio under the hood. You only have to define the API interface using annotations. Retrofit along with the retrofit_generator package generates all the boilerplate, low-level code for you.

Look at the snippet below:

@POST('/items') // 1
Future<void> createItem(
  @Body() String item, // 2
  @Header('Content-Type') String contentType, // 3
);

In the code above you have annotations defining the following properties:

  1. an HTTP method and an endpoint,
  2. the payload type, may also be e.g. @Field or @Part,
  3. the HTTP header.

Implementation of that method would contain dozens of lines of code instead of five if you used dio directly. So you can save a significant amount of time when using retrofit in comparison to writing that code manually. Moreover, the generated code is less error-prone. For example it won’t forget to pass some header to a dio instance.

Understanding Asynchronous Programming

The last theoretical part you’ll learn here is asynchronous programming. First, take a look at these two simple functions:

void main() {
  sayHello();
  print('Hello from main');
}

void sayHello() {
  print('Hello from function');
}

Run main, and you’ll see the following the output:

flutter: Hello from function
flutter: Hello from main

The order of the print statements in the code and the printed messages in the output are the same. Thus, the functions are synchronous.

Now, look at the following asynchronous code:

void main() {
  sayHello();
  print('Hello from main');
}

Future<void> sayHello() async { // 1
  await Future.delayed(const Duration(seconds: 1)); // 2
  print('Hello from function');
}

And its output:

flutter: Hello from main
flutter: Hello from function

Now, the lines are in reversed order. Hello from function appears one second later. In the code above, you have:

  1. The async keyword — an indicator that the function is asynchronous.
  2. await indicating that the function waits until the Future completes.

Note the function return type. It’s not void but Future<void>. Future holds the results of some asynchronous computation. It may be a success or a failure if the called method throws an error.

Finally, change main to the following and run it:

Future<void> main() async {
  await sayHello();
  print('Hello from main');
}

The function is asynchronous but it waits for sayHello to complete. The output is the same as in the first snippet.

You can also wait for the future to complete without using await. Take a look at this snippet:

void downloadBook() {
  Future.delayed(const Duration(seconds: 1)) // 1
      .then((_) => getBookFromNetwork()) // 2
      .then((_) => print('Books downloaded successfully')) // 3
      .catchError(print); // 4
}

Future<void> getBookFromNetwork() async { // 5
  // Get book from network
}

This code:

  1. Delays for one second.
  2. Then, calls getBookFromNetwork() after the delay completes.
  3. Either prints the success message after getBookFromNetwork() completes successfully.
  4. Or prints the error message if getBookFromNetwork() throws an error.
  5. Provides a getBookFromNetwork() stub that downloads the data from the internet (body omitted).

To sum up:

  • If you want to execute something without blocking the caller, mark the function async. Use Future return type then.
  • Use await to wait until the Future or async function call completes.

Now you know some theory, it’s time to move on to running the REST API server.

Running the REST API Server

The backend project uses the conduit framework. It’s an HTTP web server framework for building REST applications written in Dart. You won’t get into how to write server-side code using Dart. You’ll just deploy this project and focus on implementing the networking logic in the app.

To start making HTTP requests to the server, you need to deploy it on your machine. Before you begin, ensure you have access to the dart executable. Open the terminal and invoke the following command:

dart --version

You’ll get something like this as a result:

Dart SDK version: 3.0.5 (stable)

If dart isn’t accessible, you’ll get an error (Command 'dart' not found" or similar). Follow the Update your path instructions for your platform: Windows, macOS or Linux to fix the issue.

Next, run:

  1. dart pub global activate conduit in your terminal.
  2. conduit serve in your terminal from the api directory.

Those commands may take a few seconds to produce some results. After a while, you should see something like the following in your terminal:

-- Conduit CLI Version: 4.4.0
-- Conduit project version: 4.4.0
-- Starting application 'api/api'
    Channel: ApiChannel
    Config: /Users/koral/projects/bookshelf/api/config.yaml
    Port: 8888
[INFO] conduit: Server conduit/1 started.  

This command never stops in case of success. As long as the server is running, you won’t be able to use the same terminal window/tab. The client’s requests will appear in the log. You can press Control-C to stop the server.

The base URL for the server is http://localhost:8888. There isn’t any database here. It stores all the data in memory. So, if you stop and start the server again, you’ll lose all modifications.

Note: This is only suitable for local testing purposes — not for deploying on real servers exposed on the internet.