Caching GET Request calls using Flutter Cache Manager

Learn about how we can implement content caching in our Flutter application using Flutter Cache Manager plugin with a demonstrating application showcasing the setup and code in action.

Caching of less-frequently changing datasets has been a best practice in optimizing the overall page load times and off late this has become an important aspect in mobile frameworks since this improves the overall user experience a lot. Plus having cached data enables us to provide a seamless offline user experience for poor data connections or no data scenarios. This gives an elegant feel to our mobile applications as well in the UX. In this article, let’s look at how we can implement a simple caching mechanism to persist asset data in a flutter application using flutter_cache_manager plugin.

Setting up the Context:

Off late, I was asked to work on a mobile application being designed in flutter framework. Flutter is an emerging cross-platform mobile application framework powered by Google, which can be used to develop beautiful and powerful mobile applications for multiple mobile application platforms such as Android or iOS apart from web.

The framework uses dart language for its programming and designing, and the framework itself is built on the same. The dart language derives its features and facets heavily from popular scripting languages such as JavaScript, TypeScript, React and somewhat C# and Swift as well (I felt so). And so programming in Flutter has been an interesting learning for me since my first hands-on.

And then there was a scenario where in I need to cache certain GET requests from a backend API via HTTP which turn out to be a not-so-frequently updated configuration calls. And caching such requests is a best practice and is necessary when you’re into a mobile application since every network call is costly for you. And so I started to scout for how to cache requests effectively in flutter.

Caching – How it works (From the Client Purview):

In any application where an API source and a consuming client is involved, caching is a bi-directional agreement between the provider and the consumer. Let me put this in this way: when the client requests for a data from an external API such as an aspnetcore, nodejs or other backend technologies, the API supplies data along with information on how to cache and how long the data should be cached along with the returned data. These are passed down to the client by means of Response Caching Headers just before when the data is sent out of the API. This is important because the client has no idea on how long the data shall be valid or how frequently it can expect the data should change. This the client implements by looking for a Response Header along with the content called as Cache-Control. This Cache-Control comes up with the following types of instructions on how the returned needs to be cached in the client.

A sample response can look like below:


cache-control:private, max-age=3600
content-encoding:gzip
content-length:3086
content-type:application/json
etag:410a2e80d995e048f97f324205e15227
last-modified:Thu, 02 Jan 2020 07:35:45 GMT
server:Microsoft-IIS/8.5
vary:Accept-Encoding
www-authenticate:Basic Realm="test"
x-aspnet-version:4.0.30319
x-powered-by:ASP.NET

When we observe the cache-control tag, it says the max-age is 3600 which means the server tells the client to hold the data for a maximum of 3600 seconds only. After that the server cannot guarantee that the data is valid and the same. Based on this we implement our client to cache and persist data for us without having to go for exhausting API calls.

Caching in Flutter – Hello Cache_Manager:

In flutter, we can implement a cache which handles requests for us by means of a plugin called flutter_cache_manager. We add the plugin to the pubspec.yaml file, which is basically a package.json for flutter as below. Once we save the flutter, the framework automatically downloads and installs the plugin into the source code.


name: hello_world_flutter
description: A new Flutter project.

version: 1.0.0+1

environment:
  sdk: ">=2.1.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  flutter_cache_manager: ^1.1.3

dev_dependencies:
  flutter_test:
    sdk: flutter

// unnecessary sections ommitted

Once this is done, we can simply use the cache_manager for our get calls by using the DefaultCacheManager().getSingleFile() method, which is simply a HTTP GET + Cache for any GET Asset API. A Sample Http provider that works on top of Cache can look like this,


import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:http/http.dart';

class HttpProvider {
  Future<Response> getData(String url, Map<String, String> headers) async {
    var file = await DefaultCacheManager().getSingleFile(url, headers: headers);
    if (file != null && await file.exists()) {
      var res = await file.readAsString();
      return Response(res, 200);
    }
    return Response(null, 404);
  }
}

We have the DefaultCacheManager() (implemented as a singleton by default) which keeps a track of all the GET assets by means of their request URLs and the Headers. And the CacheManager talks all but about files, since all the responses are stored in the form of files. There are only two cases here; when a response is available in cache we have a File object available and when there is no cache available and caching was not possible (probably coz of a network failure) then there is null returned. And what we can do is simply wrap this over a Response object for the recepients which expect a Response from the Http provider. The header are passed so as to have authorizations if needed or have the caching-control (like as we saw before) done for the response data.

How Cache_Manager works internally?

When we make a call to the DefaultCacheManager().getSingleFile() along with the URL and the headers, the cache_manager internally looks for any cache already available with the matching URL and headers. When nothing is found (like in the initial cases) the cache_manager makes an API GET call for the resource. Once the response is fetched, it looks for any response headers of the key Cache-Control. If there is one available, it sets the ValidTill attribute for the cache as per the max-age returned by the server. If nothing is available, it sets a default cache-expiry for 7 days. We can customize this which we shall look a little further.

The cache object now created is stored and maintained along with the input headers in a data dictionary internally for faster seeking in future. And for all consecutive calls, the cache_manager looks for a cache which is available and is not_yet_expired for the URL. This way, the caching is done with minimal effort for us. When the cache expires (after the ValidTill is exceeded), the cache_manager makes a new API GET call with the supplied URL and headers. If there’s an API generated ETag present in the cached headers, it is used as well for the GET call. If the resource doesn’t change at the server level (indicated by the 304 Not Modified when we pass a ETag), the existing cache is extended off its expiry_duration.

In short a GetSingleFile() does the things below:

  1. Look for available cache data which is not yet expired
  2. Return cached if available
  3. Else download resource from the URL using the headers + ETag (If available in the expired cache metadata)
  4. Once downloaded, overwrite the expired cache with new data
  5. If there’s no modified data returned (Status code 403), extend the existing cache validity and return

Customizing the Cache_Manager:

Let’s assume we have an API which doesn’t send a max-age for us (which isn’t an expected case for an API which supports caching), we can have a custom implementation of the CacheManager to suit our customizations. We can have a MyCacheManager which extends the BaseCacheManager class, on which the DefaultCacheManager singleton actually works. It can look this below:


import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:http/http.dart' as http;
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;

// Custom Implementation of CacheManager
// by extending the BaseCacheManager abstract class
class MyCacheManager extends BaseCacheManager {
  static const key = "customCache";

  static MyCacheManager _instance;

  // singleton implementation 
  // for the custom cache manager
  factory MyCacheManager() {
    if (_instance == null) {
      _instance = new MyCacheManager._();
    }
    return _instance;
  }

  // pass the default setting values to the base class
  // link the custom handler to handle HTTP calls 
  // via the custom cache manager
  MyCacheManager._()
      : super(key,
            maxAgeCacheObject: Duration(minutes: 20),
            maxNrOfCacheObjects: 20,
            fileFetcher: _myHttpGetter);

  @override
  Future<String> getFilePath() async {
    var directory = await getTemporaryDirectory();
    return path.join(directory.path, key);
  }

  static Future<FileFetcherResponse> _myHttpGetter(String url,
      {Map<String, String> headers}) async {
    HttpFileFetcherResponse response;
    // Do things with headers, the url or whatever.
    try {
      var res = await http.get(url, headers: headers);
      // add a custom response header
      // to regulate the caching time
      // when the server doesn't provide cache-control
      res.headers.addAll({'cache-control': 'private, max-age=120'});
      response = HttpFileFetcherResponse(res);
    } on SocketException {
      print('No internet connection');
    }
    return response;
  }
}

When we use the MyCacheManager() in place of DefaultCacheManager() in our Http provider, the caching is done pretty much the same way as done before with added customizations. In this way we can implement pretty much a simple CacheManager in our flutter applications, by using the flutter_cache_manager which is light weight as well as much customizable for our requests.

Interested in learning Flutter? Check out our curated collection of articles on getting started with Flutter for beginners.


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *