Dio Interceptors in Flutter
Sometimes we don’t need complex apps, sometimes we just need an app that displays a list of items from one endpoint, and we can manage to do it with a simple method:
Summary
This context provides a detailed guide on how to use interceptors in Dio, a popular HTTP client for Flutter, to handle dynamic headers, cache, and error handling in network requests.
Abstract
The context begins by explaining that most apps require more than just a simple method to display a list of items from an endpoint. To meet these requirements, the use of interceptors in Dio is recommended. Interceptors provide specific callbacks for errors, requests, and responses, allowing for better control over network requests.
The context then delves into the configuration of Dio, which can be done using a BaseOption object that initializes a new Dio instance with a set of rules. However, interceptors cannot be added to the base configurations and must be added to the new Dio instance.
The context then explains how to add dynamic headers to requests, such as a stored key from shared preferences, and how to verify the response using interceptors. It also covers how to handle errors from the server, such as a user account deletion, by returning to the login screen.
The context concludes by explaining how to create a simple cache for network requests using interceptors, which can be further improved by implementing a persistent cache with SQL.
Bullet points
Sometimes we don’t need complex apps, sometimes we just need an app that displays a list of items from one endpoint, and we can manage to do it with a simple method:
And we don’t have any errors, there’s no need to log our responses. What about cache? Nobody cares about it!
But truth be told, almost no apps are as simple as this. Some do require more in order for us to get the response from the server or debug our application, such as:
Interceptors will help us handle this by giving us specific callbacks for errors, requests, and response.
Before diving into how we can use interceptors, let’s take a step back and look at how we can configure Dio.
Dio can be configured with a BaseOption object that lets us initialize a new Dio instance with a set of rules: connectTimeout
, receiveTimeout
and baseUrl
that will be used for every API call we make.
However, one thing that we cannot add in the base configurations (at the time the article was written) is the interceptors. For that, we need to create the new Dio instance and add the interceptors that we want in the interceptors
list.
And with this, we have set up a Dio instance that can be used for any API call that we make.
As stated in the introduction, let us suppose that the app that we are making needs a header that contains a value stored in shared preferences (that we admit that the value is dynamic, so it can change at any time) and the current time.
As a side note: Since the data can be changed, we cannot use the BaseOptions
's extra
field, which would conveniently let us access data static data that we passed to it on its creation. So, we will need to access the shared preferences each time we are making a request.
The InterceptorsWrapper
gives us the RequestOptions
object, which has the following properties:
dynamic data
String path
Map<String, dynamic> queryParameters
With this information, we can start implementing our requestInterceptor
method.
This method returns a dynamic
type that can be:
RequestOptions
object if we want to continue with the requestResponse
object if we want the app to take care of the request by itselfDioError
or dio.reject
object, that will throw an error.This will let us have the flexibility to validate each request before it’s being made, add data, and throw any error if necessary. For our case, we just need to add some data and proceed with the request.
Knock, knock. The Project Manager comes to tell you that now, for some reason, you cannot send the headers for a specific set of endpoints.
We could quickly solve this by using a switch
statement with the path
parameter. But we don't want that. We want to be able to see in the endpoint declaration if that endpoints needs, or does not need the token. So, instead of searching for the paths in the interceptor, we are going to add an auxiliary header
to each endpoint request.
Then, we will be able to verify if the request has that header, remove it, and add the token.
As it was with the Request
, we also have a specific key stored in our shared preferences with which we must verify our responses. If we cannot find the specified header or if the key is different than the one stored, we throw an error message saying that the user is no longer active.
In the same way that we created a method for the request, we can do the same for the response. The key difference is that now we are dealing with a Response
object, which in part has the same data as the Request
such as data
, headers
but it also has the statusCode
and the original Request
data.
As with the response
, we have a dynamic
type as a return for this function which can be:
Response
object if we want to continue with the requestDioError
if we to throw an error after validating the response dataWhich means that, if the boolean value for the header isUserActive
is false, we can return a DioError
object as follows:
Let us assume that we have some mechanism in our server that can delete a user account completely. In this case, the app has to return to the login screen for the user to create a new account. The error from the server has the following message: {"error":"ERROR_001"}
, and as with the Response
and Request
, we will create an interceptor to catch all the incoming errors.
Looking at the documentation, we can see that the type for the error interceptor is also dynamic
with the following specification:
DioError
object.Response
object, we can do it, in this case, our app is not aware that there has been an error with the server, and continues with the request normally.To go to a different screen, since we are dealing with the first version of the app (read: something to be refactored later on), we directly navigate the user to another screen from this class using the Navigator widget with a GlobalKey.
Creating a function for each interceptor can be an acceptable approach, but what if we have more than one response interceptor? One approach that we can have is to create a class that extends the Interceptor
class and overrides the onRequest
, onError
and onResponse
methods.
This class can then be easily added to the Dio object interceptors:
One thing we can do with the interceptors is to create a simple cache for our requests. The concept is simple: if the user is using the app normally and for some reason, there’s a network error or a connection-timeout, then we still want the user to see the feed or home page of the app.
To do that, we have to save in memory all the requests that we are making, and, when verifying for the connection timeout error or an internal error from Dio ( DioErrorType.DEFAULT
), if there's a request that we have saved for the same endpoint, with the same parameters, then we return the old response and warn the user that he's offline at the moment.
We could further improve this cache by modifying our response and give it a parameter to warn the UI that this response is now a cache, or implement a persistent cache with SQL that would let the user see the previous feed if he opens the app offline, much like what the Linkedin app does.
For the majority of the time spent creating a project, we are going to stumble on errors upon errors when making API requests. Maybe we forgot a query parameter or the header, or the body has a missing parameter or it’s just a case of the backend having a nasty bug. For all these cases, the best would be to have all the requests logged into our console so that we can easily check what has happened.
As with before, we create a new Interceptor
class, implement all the necessary methods and log all the information that we want from the requests. Furthermore, since we want our logs to easily stand out, we might want to format them to always start and end with the following:
Request
, we want to print out the query parameters and the body of the request (if available), headers and the URL;Response
, we want to print out the URL, headers, body and status codeError
, we will want the status code and the error itselfTesting it out with the JSON Placeholder website, we get the following logged:
As a further note, we are currently using the print method for displaying the logs. This might not be ideal since these logs will also appear in the production app, which means that anyone who connects the phone with this app open and runs flutter logs
will be able to see the full output. As a better alternative, we can use debugPrint
with Product Flavors as seen in this article - debugPrint and the power of hiding and customizing your logs in Dart.
And that’s it 👻 with this we have at least mastered the basics of Interceptor
s in Dio and can now add more functionality, logging and better error handling to our apps.
As always, please do tell me in the comments about what are going to be your uses for them!
Flutter developers always try to find ways to manage and perform network requests faster in their applications. Enter Dio: an urge and…
I rewrote the form validation example from the bloc repository with GetX.