Basic and advanced networking in Dart and Flutter — the Tide way. Part 6: REST API requests with retrofit. Advanced.

Feeling lost? Check out the introduction into this series.

Parts 5 and 6 of this series are dedicated to an efficient implementation of REST API requests.

This part aims to show how we at Tide customize headers for individual / selected / most / all API requests with retrofit code generation and dio interceptors.

In this part:

1. headers for individual requests
2. headers for most / all requests
3. headers for selected requests
4. pre/post request actions

For basic topics check out Part 5. It talks about:

1. simple API request
2. API request attributes
3. generic API responses

0. Prerequisites

Further code examples are built on top of the code developed in Part 5, which can be found under the part-5 tag in the Flutter Advanced Networking GitHub repository.

1. Headers for individual requests

If an API request is required to send known static headers, it can be annotated with @Headers attribute, like on line 10:

The generated .g.dart file declares a _headers map with the same values on line 11 and appends them to request headers on line 18:

Here is the trick if there is a need to specify these headers over and over. Dart allows using any const value as an annotation. So the implementation above is completely identical to the one below. Here the result of the Headers constructor call is saved to a const object popularHeaders on line 5, which is then used as an annotation on line 14:

This implementation generates the same code as above.

The @Header attribute allows providing a single dynamic request header. For an example on line 10:

the generated .g.dart file contains the same header value on line 11 and appends it to request headers on line 15:

The same trick with creating a reusable annotation is applicable to the @Header attribute. Here the known Header('header-name') is saved to a const field popularHeader, and @popularHeader annotation is used instead on line 12:

The generated code is the same. Now @popularHeader can be used in other requests without the need to repeat the header name.

Multiple dynamic headers can be provided one by one as shown above, or via the @Headers attribute, like on line 10:

The generated .g.dart file contains the usage of parameters header1Value and header2Value as headers on lines 12 and 13:

Extracting dynamic @Headers attribute value to a const annotation is also possible:

However, for it to work, the method parameters names should match values in the @popularDynamicHeaders, and that requirement is easy to forget if the annotation declaration is located in a different file.

2. Headers for most / all requests

If the same group of headers is required to be appended to all API requests, that can be done efficiently through dio interceptors. Remember the MarvelApiAuthInterceptor from Part 3, section 2 which is adding required query parameters to make authenticated requests to Marvel Comic API? The idea is the same: creating a dio interceptor that would append required static headers (or headers whose value can be obtained independently from the request) to all requests. Dynamic headers cannot really be attached through dio interceptors, because their value is only known when the API request is called.

This ExampleInterceptor appends one static header on line 16, one calculable header, different for each request on line 19, and one header obtained from the external source on line 21:

When attached to dio instance, it sends this group of headers with each request. If there are only few requests that should not contain one of the headers, for example, the /login and /forgot-password requests do not require an access-key header, the ExampleInterceptor still can be used with minor modifications, like on line 10 and 16:

Obviously, developers have to remember to update the list of _exceptions with each new request implementation. This approach may work well for small to medium applications. But we at Tide use a more advanced technique.

3. Headers for selected requests

We have quite a big mobile application with hundreds of API calls. Among other headers that are attached with each request, there are five particular headers that are sent with some or most API requests. They represent information available globally in the application, like access keys, some identifiers, information about installation, etc. For simplicity, let’s call them "header1", "header2", ... "header5".

All requests to our API require two to four of those headers attached. So, for example, requestA requires "header1" and "header2" , requestB"header2", "header3", and "header5", requestC"header2", "header3", "header4", and "header5" , and requestD requires only "header2" and "header4". Here, requestA,requestD rather mean request types than some particular requests.

Just imagine how providing these headers to individual requests with @Header attribute would look like:

To use HeadersExampleApi developers would have to obtain the same information over and over again from the commonly available sources:

Remembering that there are hundreds of requestsAs and requestsDs and alike in our project, it is hard to imagine the amount of code duplication this approach would lead to. Would not it be cool to attach dio interceptors that append headers only to required requests? But how to inform an interceptor about which requests require which headers? Ideally, the interceptor should look similar to this:

In fact, we have interceptors dedicated to each header, but for the simplicity of the example, they can all be handled in one interceptor. So, what that some condition should be? Given our project scale, the approach with an exceptions list is not a feasible solution. We needed a way to mark each request with the set of headers it requires, which dio interceptor would be able to understand. Thus, meet an @Extra attribute.

The @Extra attribute is dedicated to holding custom fields. It allows providing an extra Map<String, Object> to a request that can later be retrieved in dio interceptors. For an @Extra attribute on line 9:

the generated .g.dart file contains the same value on line 10:

Now it can be read in dio interceptor like on line 8:

We also clean up the extra map on line 16 so no other interceptor processes the same data.

Following this approach, we divided all API requests into groups based on the headers they require, and created custom annotations for each group:

When such an annotation is applied over a request, like on lines 9 and 13:

the generated .g.dart file contains unfolded values like on lines 10 and 20:

Now they can be read in dio interceptor to decide whether to append headers:

And users of HeadersExampleApi are free from providing irrelevant (from their point of view) information and can focus on passing in only the data that matters:

A reminder: there is no one-to-one match between a request and request type annotation. requestAType,requestDType annotations can be applied over any retrofit request, and will result in appending required headers if AppendHeadersInterceptor is attached to a dio instance. For example, here the getComics request from MarvelComicsApi is annotated with @requestAType attribute on line 9:

As a result, it’ll have "header1" and "header2" attached.

4. Pre/post request actions

Because our application allows managing members’ bank accounts, some operations are security-sensitive and require users to enter their pin or use biometrics. This unlocks us the ability to access encrypted storage and read some data to pass it to the backend. For simplicity let’s say we naively ask users if they agree to share secret data with a simple yes / no dialog:

The obvious implementation is to call getSecretData before calling each security-sensitive API request:

However, this approach has the same downside as in the previous section: code duplication when implementing each security-sensitive request, and taking care of irrelevant from the point of an API user information. The solution is also the same: create a dio interceptor, and find a way to inform it which requests should be preceded by a user permissions dialog.

You might think: “hold on, I know the solution: use an @Extra annotation and append another marker to required requests!”. We also thought so, but it appears only one attribute of @Extra type is allowed on top of API request implementation. If there are multiple of them like on lines 10 and 11:

only the first one is used in the generated .g.dart file:

So we had to use another attribute that could annotate a retrofit request and have an effect on the generated code, which is the @Headers attribute. The advantage of using an @Extra attribute was that even if no interceptor is attached to the dio instance and does not clean up the extra map, it’s not harmful to have something in it, because it is not used when the real request is sent to the backend. With @Headers, if no interceptor is listening for it and does not replace marking headers with something meaningful, it will be sent “as is” and may cause a misunderstanding with the backend.

So we created a new annotation:

And a dio interceptor that would wait for and process such a header:

It detects if the headers map contains a header with a special secureActionHeader value on line 14. If it does, the interceptor requests for secret data on line 16. If the request was successful, data is appended as a security-header value, and the request proceeds as normal. Otherwise, API request is rejected on line 20. We remember to clean the irrelevant header value on line 15.

Now @secureAction can be used over an API request:

And users of the API can forget about its implementation details:

The @secureAction annotation can be used over any retrofit request, so here it is annotating the getComics request:

Now each time user updates the list of comics, they are asked for confirmation.

The same approach is applicable for post-request operations, it all depends on whether the code in the interceptor is executed before super.onRequest(options, handler) call or after.

Conclusion

We now have implemented various ways of customizing API calls on a retrofit per-request level as well as globally in dio interceptors.

The final version of the code developed in this part is located under the part-6 tag in the Flutter Advanced Networking GitHub repository.

Final words

As promised, we’ve been developing an application that displays a list of Marvel comics obtained from Marvel Comic API. Here the getComics method from MarvelComicsApi is used to obtain a comics list, and the result is displayed in a really simple UI:

Check out the latest code in the Flutter Advanced Networking GitHub repository for a fully implemented Flutter application.

The tools and approaches described in this series are very close to what we have created at Tide while developing our mobile applications with Flutter. If you are as excited about this technology as we are — check out our careers page for “Mobile Engineer” positions in different locations. We really look forward to meeting like-minded passionate professionals!

--

--

Anna Leushchenko 👩‍💻💙📱🇺🇦
Tide Engineering Team

Google Developer Expert in Dart and Flutter | Author, speaker at tech events, mentor, OSS contributor | Passionate mobile apps creator