Avoid unoptimized downloads

Some of your app's users have intermittent access to the internet or have limits on the amount of information they can download onto their devices. You can encourage users to interact with your app more often by reducing the amount of data that your app needs to download.

The most fundamental way to reduce your downloads is to download only what you need. In terms of data, that means implementing REST APIs that allow you to specify query criteria that limit the returned data by using parameters such as the time of your last update.

Similarly, when downloading images, it's good practice to reduce the size of images server-side, rather than downloading full-sized images that are reduced on the client.

Cache HTTP responses

Another important technique is to avoid downloading duplicate data. You can reduce the likelihood of downloading the same piece of data repeatedly by using caching. By caching your app's data and resources, you create a local copy of the information that your app needs to reference. If your app needs to access the same piece of information multiple times over a short time period, you need to download it into the cache only once.

It's important to cache as aggressively as possible in order to reduce the total amount of data that you download. Always cache static resources, including on-demand downloads such as full-size images, for as long as reasonably possible. On-demand resources should be stored separately to enable you to regularly flush your on-demand cache to manage its size.

To ensure that your caching doesn't result in your app displaying stale data, use the appropriate HTTP status codes and headers, such as the ETag and Last-Modified headers. This allows you to determine when the associated content should be refreshed. For example:

Kotlin

// url represents the website containing the content to place into the cache.
val conn: HttpsURLConnection = url.openConnection() as HttpsURLConnection
val currentTime: Long = System.currentTimeMillis()
val lastModified: Long = conn.getHeaderFieldDate("Last-Modified", currentTime)

// lastUpdateTime represents when the cache was last updated.
if (lastModified < lastUpdateTime) {
    // Skip update
} else {
    // Parse update
    lastUpdateTime = lastModified
}

Java

// url represents the website containing the content to place into the cache.
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
long currentTime = System.currentTimeMillis();
long lastModified = conn.getHeaderFieldDate("Last-Modified", currentTime);

// lastUpdateTime represents when the cache was last updated.
if (lastModified < lastUpdateTime) {
    // Skip update
} else {
    // Parse update
    lastUpdateTime = lastModified;
}

You can configure some networking libraries to respect these status codes and headers automatically. When using OkHttp, for example, configuring a cache directory and cache size for the client will enable the library to use HTTP caching, as shown in the following code sample:

Kotlin

val cacheDir = Context.getCacheDir()
val cacheSize = 10L * 1024L * 1024L // 10 MiB
val client: OkHttpClient = OkHttpClient.Builder()
    .cache(Cache(cacheDir, cacheSize))
    .build()

Java

File cacheDir = Context.getCacheDir();
long cacheSize = 10L * 1024L * 1024L; // 10 MiB
OkHttpClient client = new OkHttpClient.Builder()
    .cache(new Cache(cacheDir, cacheSize))
    .build();

With the cache configured, you can serve fully-cached HTTP requests directly from local storage, eliminating the need to open a network connection. Conditionally-cached responses can validate their freshness from the server, eliminating the bandwidth cost associated with the download. Uncached responses get stored in the response cache for future requests.

You can cache non-sensitive data in the unmanaged external cache directory by using Context.getExternalCacheDir(). Alternatively, you can cache data in the managed, secure application cache by using Context.getCacheDir(). Note that this internal cache may be flushed when the system is running low on available storage.

Use a repository

For a more sophisticated approach to caching, consider the Repository design pattern. This involves creating a custom class, known as a Repository, which provides an API abstraction over some specific data or resource. The repository may initially fetch its data from various sources, such as a remote web service, but provides callers with a cached version of the data in subsequent calls. This layer of indirection allows you to provide a robust caching strategy that's specific to your app. For more information about using the Repository pattern within your app, see the Guide to app architecture.