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.
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
30 mins
- Getting Started
- Exploring the Project
- Defining Important Terms
- Understanding Network Requests and Responses
- Understanding RESTful APIs
- Understanding Endpoints
- Understanding HTTP Methods
- Understanding HTTP Status Codes
- Understanding JSON
- Discovering Retrofit
- Understanding Asynchronous Programming
- Running the REST API Server
- Preparing for Development
- Adding Dependencies
- Generating Data Model Classes
- Implementing the REST Client Skeleton
- Performing GET Request
- Performing DELETE Request
- Performing POST and PUT Requests
- Where to Go From Here?
Preparing for Development
Before you begin writing the actual logic you have to add the dependencies and prepare the model classes for deserialization and serialization from/to JSON.
Adding Dependencies
Open pubspec.yaml in the starter project root directory (not the API).
Next, find # TODO: add runtime dependencies
in the dependencies
block and replace it with:
dio: ^5.3.2 # 1
retrofit: ^4.0.1 # 2
json_annotation: ^4.8.1 # 3
Here, you’ve added the runtime dependencies. You can use them in the code of your app. Here are their purposes:
- dio is an HTTP client used for making requests.
- retrofit is a type-safe wrapper over dio for easily generating network requests making code.
- json_annotation contains the annotations used by the json_serializable package, described in the coming paragraphs.
Moving forward, find # TODO: add build time dependencies
in the dev_dependencies
and place the following code there:
build_runner: ^2.4.6
json_serializable: ^6.7.1
retrofit_generator: ^7.0.8
Those dependencies are available only during build time. You can’t use them from the app code like widgets. They’re for generating the files (build_runner
) and code used for JSON parsing (json_serializable
) and HTTP response (retrofit_generator
) handling.
Don’t forget to run flutter pub get
from the terminal or IDE. You won’t be able to use the dependencies otherwise.
Generating Data Model Classes
The response from the server is serialized in the form of JSON. Dart can’t interpret it out of the box. So, you have to parse — or, deserialize — the JSON to a Dart data model class.
You could do it manually. For example, find each field such as author, treat it as a string and do the same with other fields. Finally, create the data model class instance out of them.
Then, to get a response, you have to construct the URL and pass it to the HTTP client. Next, read the status code to determine if a request was successful, handle the errors and so on.
However, you won’t do it that way: you’ll use code generation to generate all that boilerplate code! You won’t get into details of code generation in this tutorial. But you can read more about it in our other article: Flutter Code Generation: Getting Started.
Though json_serializable will generate the code related to JSON parsing for you, you have to give it some instructions about your data models. Open lib/model/book.dart and replace its content with the following:
import 'package:json_annotation/json_annotation.dart';
part 'book.g.dart'; // 1
@JsonSerializable() // 2
class Book {
@JsonKey(includeIfNull: false) // 3
final int? id;
final String title;
final String author;
final String? description;
factory Book.fromJson(Map<String, dynamic> json) => _$BookFromJson(json); // 4
const Book(
{this.id, required this.title, required this.author, this.description});
Map<String, dynamic> toJson() => _$BookToJson(this); // 5
}
In the code above, you:
- Indicate that a part of the code is in another file — yet to be generated by
build_runner
. - Add an annotation marking the class as deserializable/serializable from/to JSON.
- Annotate that
null
values should be omitted from the JSON serialization. - Create a factory constructor for deserializing from JSON.
- Include a method for serializing to JSON.
When making a request to create a new book, the book doesn’t have an ID yet. The backend will assign it. So, it makes no sense to send explicit null values, which is the default behavior. Adjusting the @JsonSerializable
and @JsonKey
annotations parameters enables you to customize the serialization/deserialization process.
For example, you can change the field name in a serialized form. It’s especially handy if the backend uses snake_case
while your Dart code uses camelCase
. Read more about customizations in the package’s official documentation.
Next, run the following command in the terminal to generate the code:
dart run build_runner build --delete-conflicting-outputs
Without that, you’ll get compilation errors and red lines in the IDE!
Implementing the REST Client Skeleton
The last preparation step is a REST client stub. Create a lib/network/rest_client.dart file with the following content:
import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';
part 'rest_client.g.dart';
@RestApi(baseUrl: 'http://localhost:8888') // 1
abstract class RestClient {
factory RestClient(Dio dio) = _RestClient; // 2
}
The code above defines:
- The base URL for all the endpoints, so you don’t need to repeat it for each of them.
- The factory constructor you’ll use to create the client instance.
Note the localhost
hostname is valid only on the same machine where the server is running. It works on the desktop, web and iOS simulators (if you’re on macOS). In case of physical iOS devices you have to change the localhost
to the IP address of your computer. Both devices have to be in the same local network. If you’re using Wi-Fi, it can’t use client isolation or the guest network.
For Android emulators, you have to turn on port forwarding. To do so, use the following command: adb reverse tcp:8888 tcp:8888
. With physical Android devices, use a port forwarding or an IP of your computer.
Then, run the command to generate the code as before:
dart run build_runner build --delete-conflicting-outputs
That generates the RestClient
you’ll use to make the network requests.
Performing GET Request
Now you’re ready to implement the actual networking logic. Open lib/ui/bookshelf_screen.dart and replace // TODO: add book stream controller
with the following fragment:
final _bookStreamController = StreamController<List<Book>>();
The StreamController connects the data source with the UI layer. It’s like a pipe. Everything you add to it will appear at the other end — sink. Start with connecting the source. To do that, find // TODO: implement GET network call
and replace the refreshBooks
method with the following expression:
Future<void> refreshBooks() => _dataSource // 1
.getBooks() // 2
.then(_bookStreamController.add) // 3
.catchError(_bookStreamController.addError); // 4
Breaking down the code above:
- The method returns a future, signalling it’s asynchronous.
- The network call to get books also returns a future.
- Add the list of books to the stream using the stream controller when the request succeeds.
- Add the error to the stream controller when the request fails.
Because the method is asynchronous, its invocation won’t block the UI. You can still use the app while a request is in progress.
Next, connect the other end of the stream. Find // TODO: bind data source
, replace it and the line below it with:
stream: _bookStreamController.stream,
Now the StreamBuilder will listen to the events and invoke the builder
method on each update.
Moving on, locate // TODO: clean up resources
in dispose
and change it to this code:
await _bookStreamController.close();
That closes the stream controller to avoid memory leaks.
Next, open lib/network/rest_client.dart
and add the following method:
@GET('/books')
Future<List<Book>> getBooks();
Remember to import lib/model/book.dart
at the top with import '../model/book.dart';
.
Regenerate the code using build_runner:
dart run build_runner build --delete-conflicting-outputs
The generated implementation will appear in rest_client.g.dart
. Open the file, observe getBooks()
and see how many lines of code you don’t need to write yourself!
Finally, open lib/network/data_source.dart and import dio and the REST client at the top:
import 'package:dio/dio.dart';
import 'rest_client.dart';
Find // TODO: implement GET request
and replace the commented method with this code:
final _restClient = RestClient(Dio()); // 1
Future<List<Book>> getBooks() => _restClient.getBooks(); // 2
Breaking down the code above, you:
- Instantiate the
RestClient
, used for making requests. - Use the client to make a GET request call to get books.
Run the app. You’ll see something like this: