Flutter Web API Request Local Cache

I have already made an article about how to develop a local caching for Firestore. Now it’s time to make the same thing, but for everything else. So I am going to show you how you can create local caching for your API calls.

The problem

You can easily find yourself in my position, where I created a web app, but a web app is really different than a mobile app. Mobile apps cannot be resized like you could resize a browser. When you resize the browser – and did not implement a proper state management- every API call that is on the page, will be reran.

As you can see resizing is flooding the network with API calls. The good thing is, you can get rid of these requests pretty easily. First you have to recognize that there’s a problem, but trust me you will see it, it laggs like crazy when you are resizing. Second, the solution is obvious. Just don’t let those request get out to the network. Third is implementation.

Should you cache?

Or rather when should you cache? Simple, you should cache everytime, when the API you are requesting is not a POST, modify something and gives you back some data that you would use. The reason is, if you cache that, you won’t be able to see the modified data, because it will be your previous cached data, that is outdated at this point. But we will implement a way around it.

Implementation

You could implement a local cache by modifying the flutter_service_worker.js, but that is a bit of a hassle. That file is only available once you have built your project, and everytime you build it, it gets overwritten. Secondly it is not Flutter, but js.

I have chosen the approach of creating an interceptor within Flutter. An interceptor is some kind of gateway, where your network packages will be monitored. You can find and create interceptors in the Dio package, but other packages also have similar approaches, like Chopper, which also has them.

You can monitor out going requests, incoming responses and also incoming errors as well. For the local cache, we are just going to need the onRequest and onResponse methods.

import 'package:DOChallenge/core/network/entities/cache_response.dart';
import 'package:dio/dio.dart';

const DEFAULT_CACHE_DURATION 5;

class LocalCacheInterceptor implements Interceptor {
  final int cacheDuration;
  static Map<StringCacheResponse> cache = Map<StringCacheResponse>();

  LocalCacheInterceptor({this.cacheDuration = DEFAULT_CACHE_DURATION});

  @override
  Future onError(DioError err) {
    return Future.value(err);
  }

  @override
  Future onRequest(RequestOptions options) async {
    String key = options.path;
    
    if (options.extra["isForced"] ?? false) {
      return Future.value(options);
    }

    if (cache.containsKey(key)) {
      if (Duration(seconds: cacheDuration) > DateTime.now().difference(cache[key].timestamp)) {
        return Future.value((cache[key].response as Response));
      }
    }

    return Future.value(options);
  }

  @override
  Future onResponse(Response response) {
    
    final key = response.request.path;
    if (cache.containsKey(key)) {
      if (Duration(seconds: cacheDuration) < DateTime.now().difference(cache[key].timestamp)) {
        cache[key] = CacheResponse(timestamp: DateTime.now(), response: response);
      }
    } else {
      cache[key] = CacheResponse(timestamp: DateTime.now(), response: response);
    }
    return Future.value(cache[key].response);
  }
}

We are saving the response in a Map<String, CacheResponse> object, where the key is the path of the request, and the value is the response plus a timestamp.

class CacheResponse {
  final DateTime timestamp;
  final dynamic response;

  CacheResponse({this.response, this.timestamp});
}

And we are checking in the onRequest function, whether the map contains a response, and the ellapsed time is smaller than the cache duration.

Now for the POST problem I have described, you can use the extra option, and you can set the isForced to always get the data from the web, and not from your cache.

Example

You don’t need anything other than your API call, this example was created using the Dio package. But you can use it everywhere, you just need the create the interceptor.

Future<ChallengegetChallenge(int challengeId) async {
  Response response = await db.server.get('/doc/pub/challenge' + challengeId.toString());
  return Challenge.fromJson(response.data);
}

Results

Conclusion

We have created a way of improving our wb application, with local cahcing API requests. You could modify the flutter_service_worker.js file to implement caching, but I think the previous method is way simpler, and more usable. So feel free the copy the code and make stuff with it.