In the Open Event Android App, a live feed from the event’s Facebook page was recently implemented. Since it was a live feed, it was decided that it was futile to store it in the Realm database of the app. The data of the live feed didn’t persist anywhere, hence the feed used to be empty when the app ran without the internet connection.
To solve the problem of data persistence, it was decided to store the feed in the cache. Now, there were two paths before us – use retrofit okhttp cache management or use volley. Since retrofit is used to make the API requests in the app, we used the former. To implement caching with retrofit, its API response should include the cache control header. Since it was not a response generated by a personal server, interceptors were needed to force change the request.
Interceptors
Interceptors are a powerful mechanism that can monitor, rewrite, and retry calls. The solution was to use interceptors to rewrite the calls to force use of cache. Two interceptors were added, application interceptor for the request and the network interceptor for the response.
Implementation
Create a cache file to store the response.
private static Cache provideCache() { Cache cache = null; try { cache = new Cache(new File(OpenEventApp.getAppContext().getCacheDir(), "facebook-feed-cache"), 10 * 1024 * 1024); // 10 MB } catch (Exception e) { Timber.e(e, "Could not create Cache!"); } return cache; }
Create a network interceptor by chaining the response with the cache control header and removing the pragma header to force use of cache.
private static Interceptor provideCacheInterceptor() { return chain -> { Response response = chain.proceed(chain.request()); // re-write response header to force use of cache CacheControl cacheControl = new CacheControl.Builder() .maxAge(2, TimeUnit.MINUTES) .build(); return response.newBuilder() .removeHeader("Pragma") .header(CACHE_CONTROL, cacheControl.toString()) .build(); }; }
Create an application interceptor by chaining the request with the cache control header for stale responses and removing the pragma header to make the feed available for offline usage.
private static Interceptor provideOfflineCacheInterceptor() { return chain -> { Request request = chain.request(); if (!NetworkUtils.haveNetworkConnection(OpenEventApp.getAppContext())) { CacheControl cacheControl = new CacheControl.Builder() .maxStale(7, TimeUnit.DAYS) .build(); request = request.newBuilder() .removeHeader("Pragma") .cacheControl(cacheControl) .build(); } return chain.proceed(request); }; }
Finally add the cache and the two interceptors while building the okhttp client.
OkHttpClient okHttpClient = okHttpClientBuilder.addInterceptor(new HttpLoggingInterceptor() .setLevel(HttpLoggingInterceptor.Level.BASIC)) .addInterceptor(provideOfflineCacheInterceptor()) .addNetworkInterceptor(provideCacheInterceptor()) .cache(provideCache()) .build();
Conclusion
Working of apps without the internet connection builds up a strong case for corner cases while testing. It is therefore critical to persist data however small to avoid crashes and bad user experience.
Resources
- Complete code reference https://github.com/fossasia/open-event-android/pull/1750
- Tutorial on etags in response https://futurestud.io/tutorials/retrofit-2-activate-response-caching-etag-last-modified
- Tutorial on caching in Retrofit https://caster.io/episodes/retrofit-2-offline-cache/
- Retrofit Documentation http://square.github.io/retrofit/
- Okhttp Interceptors Documentation https://github.com/square/okhttp/wiki/Interceptors