A key part of application development is gracefully handling network requests. The response returned by the network may include unexpected results, and to have a good user experience, you need to handle edge cases well in advance.
In this article, we will look at how to process REST API requests in Flutter using Dio packages.
What is Dio?
Dio is a powerful HTTP client for Dart. It supports interceptors, global configuration, FormData, request cancellation, file downloads, timeouts, and more. Flutter provides an HTTP package that is great for performing basic network tasks, but can be quite intimidating to use when handling some advanced functions. Dio, by contrast, provides an intuitive API that makes it easy to perform advanced networking tasks.
Begin to use
Let’s start by creating a new Flutter project. Use the following command.
flutter create dio_networking
Copy the code
You can open the project with your favorite IDE, but for this example, I’ll use VS Code.
code dio_networking
Copy the code
Add the Dio package to your pubspec.yaml file.
Dependencies: dio: ^ 4.0.0Copy the code
Replace the contents of your main.dart file with the following.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Dio Networking', theme: ThemeData( primarySwatch: Colors.blue, ), debugShowCheckedModeBanner: false, home: HomePage(), ); }}Copy the code
We’ll define a HomePage class after we get the web data.
Now, let’s take a look at the network data we’ll use for the demonstration.
Test with API data
We will use the REQ | RES to test our network data, because it provides you with a sample of the user data of hosting a REST API, and allows you to do all kinds of network operation test.
We’ll start by doing a simple GET request to GET the Single User data. The endpoints that you need to do that are.
GET https://reqres.in/api/users/<id>
Copy the code
Note that
must be replaced by an integer value that corresponds to and is used to find a particular user.
Here is what a sample JSON response looks like after a successful request.
{
"data": {
"id": 2,
"email": "[email protected]",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
}
}
Copy the code
Define a model class
If you want to easily process data returned from REST API requests, you’ll want to define a model class.
For now, let’s just define a simple class to store data for a single user. You can alternate between pure Dart code or a library without making any other changes in the same sample application. We will manually define a model class like this.
class User {
User({
required this.data,
});
Data data;
factory User.fromJson(Map<String, dynamic> json) => User(
data: Data.fromJson(json["data"]),
);
Map<String, dynamic> toJson() => {
"data": data.toJson(),
};
}
class Data {
Data({
required this.id,
required this.email,
required this.firstName,
required this.lastName,
required this.avatar,
});
int id;
String email;
String firstName;
String lastName;
String avatar;
factory Data.fromJson(Map<String, dynamic> json) => Data(
id: json["id"],
email: json["email"],
firstName: json["first_name"],
lastName: json["last_name"],
avatar: json["avatar"],
);
Map<String, dynamic> toJson() => {
"id": id,
"email": email,
"first_name": firstName,
"last_name": lastName,
"avatar": avatar,
};
}
Copy the code
To prevent unnoticed errors that can occur when you manually define, you can use JSON serialization and automatically generate factory methods.
For this, you will need the following package.
[json_serializable](https://pub.dev/packages/json_serializable)
[json_annotation](https://pub.dev/packages/json_annotation)
[build_runner](https://pub.dev/packages/build_runner)
Add them to your pubspec.yaml file.
Dependencies: json_annotation: ^4.0.1 dev_dependencies: json_serializable: ^4.1.3 build_runner: ^2.0.4Copy the code
Split the user and data classes into two Dart files, user.dart and data.dart, and modify their contents.
The contents of the User class will look like this.
import 'package:json_annotation/json_annotation.dart';
import 'data.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User({
required this.data,
});
Data data;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Copy the code
The contents of the Data class will look like this.
import 'package:json_annotation/json_annotation.dart';
part 'data.g.dart';
@JsonSerializable()
class Data {
Data({
required this.id,
required this.email,
required this.firstName,
required this.lastName,
required this.avatar,
});
int id;
String email;
@JsonKey(name: 'first_name')
String firstName;
@JsonKey(name: 'last_name')
String lastName;
String avatar;
factory Data.fromJson(Map<String, dynamic> json) => _$DataFromJson(json);
Map<String, dynamic> toJson() => _$DataToJson(this);
}
Copy the code
The fromJson and toJson methods will be generated by the JSON_serialIZABLE package. Properties of some classes are annotated as @jsonKey because the names defined in the map (and the names returned by API requests) are different from their property names.
You can use the following command to trigger code generation.
flutter pub run build_runner build
Copy the code
Keep the code generator running on the server so that any new changes to the class automatically trigger code generation. Use the following command to do this.
flutter pub run build_runner serve --delete-conflicting-outputs
Copy the code
The delete-Conflicting – Outputs flag helps regenerate part of the generated class when any conflicts are found.
Initialize the Dio
You can create a separate class that contains methods to perform network operations. This helps separate functional logic from user interface code.
To do this, create a new file dio_client.dart that contains the DioClient class.
class DioClient {
// TODO: Set up and define the methods for network operations
}
Copy the code
You can initialize Dio in the following way.
import 'package:dio/dio.dart';
class DioClient {
final Dio _dio = Dio();
}
Copy the code
Define the base URL of the API server.
import 'package:dio/dio.dart';
class DioClient {
final Dio _dio = Dio();
final _baseUrl = 'https://reqres.in/api';
// TODO: Add methods
}
Copy the code
Now we can define the methods needed to perform the network request.
Defining GET requests
We will define a method to retrieve data from an API for a single user by passing an ID.
Future<User> getUser({required String id}) async {
// Perform GET request to the endpoint "/users/<id>"
Response userData = await _dio.get(_baseUrl + '/users/$id');
// Prints the raw data returned by the server
print('User Info: ${userData.data}');
// Parsing the raw JSON data to the User class
User user = User.fromJson(userData.data);
return user;
}
Copy the code
The above works, but if there are any coding errors here, the application will crash when you run it.
A better and more practical approach is to wrap the get() method in a try-catch block.
Future<User? > getUser({required String id}) async { User? user; try { Response userData = await _dio.get(_baseUrl + '/users/$id'); print('User Info: ${userData.data}'); user = User.fromJson(userData.data); } on DioError catch (e) { // The request was made and the server responded with a status code // that falls out of the range of 2xx and is also not 304. if (e.response ! = null) { print('Dio error! '); print('STATUS: ${e.response? .statusCode}'); print('DATA: ${e.response? .data}'); print('HEADERS: ${e.response? .headers}'); } else { // Error due to setting up or sending the request print('Error sending request! '); print(e.message); } } return user; }Copy the code
In this example, we also make User null so that in the event of any errors, the server will return NULL instead of any actual User data.
To display user data, we must create a HomePage class. Create a new file called home_page.dart and add the following to it.
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final DioClient _client = DioClient(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('User Info'), ), body: Center( child: FutureBuilder<User? >( future: _client.getUser(id: '1'), builder: (context, snapshot) { if (snapshot.hasData) { User? userInfo = snapshot.data; if (userInfo ! = null) { Data userData = userInfo.data; return Column( mainAxisSize: MainAxisSize.min, children: [ Image.network(userData.avatar), SizedBox(height: FirstName} ${userinfo.data.lastName}', style: TextStyle(fontSize: ${userinfo.data.lastname}), style: TextStyle(fontSize: ${userinfo.data.lastname}) 16.0),), Text(userdata.email, style: TextStyle(fontSize: 16.0),),); } } return CircularProgressIndicator(); },),),); }}Copy the code
In the _HomePageState class, you instantiate DioClient first. Then, in the build method, a FutureBuilder is used to retrieve and display the user data. At the same time, the results of get a CircularProgressIndicator will display.
Defining a POST request
You can use POST requests to send data to the API. Let’s try sending a request and creating a new user.
First, I’ll define another model class, because the properties of this JSON data will be different from the User model class we defined earlier, and will be used to process the User information we want to send.
import 'package:json_annotation/json_annotation.dart';
part 'user_info.g.dart';
@JsonSerializable()
class UserInfo {
String name;
String job;
String? id;
String? createdAt;
String? updatedAt;
UserInfo({
required this.name,
required this.job,
this.id,
this.createdAt,
this.updatedAt,
});
factory UserInfo.fromJson(Map<String, dynamic> json) => _$UserInfoFromJson(json);
Map<String, dynamic> toJson() => _$UserInfoToJson(this);
}
Copy the code
Specify a method in the DioClient class to create a new user.
Future<UserInfo? > createUser({required UserInfo userInfo}) async { UserInfo? retrievedUser; try { Response response = await _dio.post( _baseUrl + '/users', data: userInfo.toJson(), ); print('User created: ${response.data}'); retrievedUser = UserInfo.fromJson(response.data); } catch (e) { print('Error creating user: $e'); } return retrievedUser; }Copy the code
This requires a UserInfo object as a parameter, which is then sent to the/Users endpoint of the API. It returns a response containing information about the newly created user and the date and time of creation.
Defining a PUT request
You can update data in the API server by using PUT requests.
To define a new method to update a user in the DioClient class, we must pass the updated UserInfo object along with the ID of the user we want to apply the update to.
Future<UserInfo? > updateUser({ required UserInfo userInfo, required String id, }) async { UserInfo? updatedUser; try { Response response = await _dio.put( _baseUrl + '/users/$id', data: userInfo.toJson(), ); print('User updated: ${response.data}'); updatedUser = UserInfo.fromJson(response.data); } catch (e) { print('Error updating user: $e'); } return updatedUser; }Copy the code
The code above will send a PUT request to endpoint/Users /< ID >, along with the UserInfo data. It then returns the updated user information and the date and time of the update.
Defining a DELETE request
You can remove some data from the server by using a DELETE request.
Define a new method in the DioClient class to remove a user from the API server by passing the user ID.
Future<void> deleteUser({required String id}) async {
try {
await _dio.delete(_baseUrl + '/users/$id');
print('User deleted!');
} catch (e) {
print('Error deleting user: $e');
}
}
Copy the code
Choose and define your base
Instead of passing the endpoint through baseUrl every time, you can define it in BaseOptions and pass it once when you instantiate Dio.
To do this, you initialize Dio as follows.
final Dio _dio = Dio(
BaseOptions(
baseUrl: 'https://reqres.in/api',
connectTimeout: 5000,
receiveTimeout: 3000,
),
);
Copy the code
This method also provides various other customizations — in this same example, we have defined connectTimeout and receiveTimeout for the request.
Upload a file
Dio makes uploading files to the server much easier. It can handle multiple files uploaded simultaneously and has a simple callback to track their progress, making it easier to use than HTTP packages.
You can easily upload files to the server using FormData and Dio. Here is an example of sending an image file to the API, and what it looks like.
String imagePath; FormData formData = FormData.fromMap({ "image": await MultipartFile.fromFile( imagePath, filename: "upload.jpeg", ), }); Response response = await _dio.post( '/search', data: formData, onSendProgress: (int sent, int total) { print('$sent $total'); });Copy the code
The interceptor
You can intercept Dio requests, responses, and errors before processing them by using THEN or catchError. In a real-world scenario, interceptors are useful for authorization with JSON Web Tokens(JWT), parsing JSON, handling errors, and easily debugging Dio network requests.
You can run interceptors by overriding callbacks in three places: onRequest,onResponse, and onError.
For our example, we’ll define a simple interceptor to log different types of requests. Create a new class called Logging that extends from the Interceptor.
import 'package:dio/dio.dart'; class Logging extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { print('REQUEST[${options.method}] => PATH: ${options.path}'); return super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { print( 'RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}', ); return super.onResponse(response, handler); } @override void onError(DioError err, ErrorInterceptorHandler handler) { print( 'ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}', ); return super.onError(err, handler); }}Copy the code
Here, we overwrite the various callbacks triggered by Dio requests and add a print statement to each callback to log the request in the console.
Add interceptors to Dio during initialization.
final Dio _dio = Dio( BaseOptions( baseUrl: 'https://reqres.in/api', connectTimeout: 5000, receiveTimeout: 3000, ), ).. interceptors.add(Logging());Copy the code
The result of logging in the Debug console would look like this.
conclusion
Networking with Dio in the Flutter feels easy, and it handles many edge situations with grace. Dio makes it easier to handle multiple simultaneous network requests, all backed by an advanced error-handling technology. It also allows you to avoid the template code required to use HTTP packages to track any file upload progress. There are all sorts of other advanced customizations that you can do with the Dio package that are beyond our scope here.
Thank you for reading this article! If you have any suggestions or questions about this article or examples, please feel free to contact me on Twitter or LinkedIn. You can also find the repository for the sample application on my GitHub.
The postNetworking in Flutter using Dioappeared first onLogRocket Blog.