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 requestsA
s and requestsD
s 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!