Flutter + Dio Framework + Best practices

Erik
Dreamwod tech
Published in
2 min readOct 8, 2021

This article will describe best practices for REST communication in Flutter using the Dio http package. The Dreamwod app used the standard http package but we recently migrated to the Dio http package, this article will describe why and some best practices around the architecture.

Background

  • The Dreamwod app is talking with a backend Rest API.
  • The API is secured with JWT Tokens. One refresh token, long TTL, is exchanged to access tokens with short TTL.

Why Dio?

The primary reasons why we migrated from the standard http package to Dio are:

  • It’s easier to add interceptors when handling requests and errors (i.e when refreshing JWT tokens).
  • Less boilerplate code.
  • Cleaner code.

Architecture

Our flutter app is structured in the following way:

  • lib/models: RPC and models with the standard JSON-serializable annotations.
Example of a RPC model
  • lib/services: We use one service for every part of the API, for example, user_service.dart, about_service.dart, notification_service.dart etc.
Example of a service
  • lib/api: Services are using an API class for communicating with the backend. We have two Dio clients in the class, one is used for all requests except when the token is refreshed, then we use a dedicated Dio client.
API class with refresh of tokens

Handling refresh of tokens

We have a 15 minutes time-to-live on our access tokens and we refresh them if they have less than 1 minute remaining, see above. We use requestlock.lock() and requestlock.unlock() so we don’t have concurrent refresh requests.

The package jwt_decode can be used to parse the expiration time from a JWTtoken.

Error handling

Using exceptions makes it easier to catch and handle different kinds of errors. Our error interceptor below which handles different HTTP status codes and converts them to appropriate exceptions.

Error interceptor

Example of a “catch all” try-catch block.

try {
var user = await UserService().getUser();

yield UserLoaded(user: user);
} on Exception catch (e) {
yield LoadUserFailed(error: e.toString());
}

Example of a when different exceptions are caught.

try {
final response =
await UserService().login(event.email, event.password);

//... other authentication code
yield SignInSuccess();
} on UnauthorizedException {
yield SignInFailure('Invalid email or password. Please try again.');
} catch (error) {
yield SignInFailure(error.toString());
}
}

Summary

That's it! Hope it helps!

--

--

Erik
Dreamwod tech

Developer, backend, frontend, ML. Likes crossfit and training. Building on the app dreamwod.app.