avatarGonçalo Palma

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

  • Interceptors in Dio can handle dynamic headers, cache, and error handling in network requests.
  • Dio can be configured using a BaseOption object that initializes a new Dio instance with a set of rules.
  • Interceptors cannot be added to the base configurations and must be added to the new Dio instance.
  • Dynamic headers can be added to requests using interceptors, such as a stored key from shared preferences.
  • The response can be verified using interceptors, and errors from the server can be handled by returning to the login screen.
  • A simple cache for network requests can be created using interceptors, which can be further improved by implementing a persistent cache with SQL.

Dio Interceptors in Flutter

Photo by Alina Grubnyak on Unsplash

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:

  • Sending dynamic headers to the server. For example, a stored key from shared preferences;
  • Checking each response header and saving its values locally;
  • Verifying the errors sent from the server and directly map them to Error classes that our app can handle;
  • Additionally, we might want to add a simplified cache to our app so that if the connection is timed out or if the user does not have internet access, we can display the previous response for that request;
  • And we can also add logging to all our responses and requests.

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 Configuration

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.

Adding Dynamic Headers

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:

  • Request dynamic data
  • Url String path
  • Query Parameters Map<String, dynamic> queryParameters

With this information, we can start implementing our requestInterceptor method.

This method returns a dynamic type that can be:

  • The RequestOptions object if we want to continue with the request
  • A Response object if we want the app to take care of the request by itself
  • a DioError 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.

Verifying the Response

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:

  • The Response object if we want to continue with the request
  • A DioError if we to throw an error after validating the response data

Which means that, if the boolean value for the header isUserActive is false, we can return a DioError object as follows:

Verifying Errors from the Server

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:

  • If we want to continue the request with an error, we return the DioError object.
  • If we want to resolve the request and return a 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.

Extending the Interceptor Class

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:

Putting it all together — Creating a simple Cache

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.

Bonus Tip — Logging Everything Dio Related

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:

  • For our Request, we want to print out the query parameters and the body of the request (if available), headers and the URL;
  • For the Response, we want to print out the URL, headers, body and status code
  • As for the Error, we will want the status code and the error itself

Testing 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
Dart
Dio
Interceptors
Networking
Recommended from ReadMedium