В предыдущей статье я описал процесс реализации механизма кэширования новостных тем в приложении TrendNow с помощью базы данных Room. Если вы пропустили эту статью, можете ознакомиться с ней здесь:

Реализация кэширования новостных тем в приложении TrendNow. Часть 5

Эта статья посвящена продолжению решения проблемы ограничения скорости API путем реализации механизма кэширования трендовых новостей.

Напомню, что исходный код в репозитории Github может отличаться от приведенных здесь фрагментов кода, поскольку я работал над ним до написания этой статьи.

Что я собираюсь сделать?

  • Использовать класс Cache библиотеки OkHttp для сохранения ответа на трендовые новости.
  • Обеспечить загрузку в приложение трендовых новостей из кэшированного ответа OkHttp, когда он будет доступен.

Почему для кэширования новостных тем подходит Room, а для кэширования трендовых новостей — OkHttp Cache?

  • Я использовал Room для кэширования новостных тем (о чем рассказал в предыдущей статье), чтобы сохранить ответ в течение длительного периода времени. В данном случае использование Room уместней по сравнению с OkHttp Cache, который удаляет кэш при заполнении хранилища кэша.
  • Трендовые новости буду кэшировать только в течение короткого периода. Поэтому достаточно использовать OkHttp Cache.

Активация OkHttp Cache

Сначала надо отредактировать клиент OkHttp в Hilt-модуле — NetworkModule.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

private const val CACHE_DIR = "news_cache"
private const val CACHE_SIZE_IN_MB = 50L // 50MB

@Provides
@Singleton
fun provideOkHttpClient(
@ApplicationContext context: Context // inject application context
): OkHttpClient {
// определить путь каталога кэша
val file = File(context.filesDir, CACHE_DIR)
// установка каталога и размера кэша
val cache = Cache(file, CACHE_SIZE_IN_MB * 1024 * 1024)
return OkHttpClient.Builder()
// установка OkHttp Cache
.cache(cache)
...
}

...
}
  • Важное замечание: использование context.cacheDir является достаточным для каталога OkHttp Cache, если у вас нет веских причин не использовать его.
  • context.filesDir: использование каталога files вместо каталога cache позволяет предотвратить очистку кэша новостей с помощью типичных сторонних приложений для чистки телефона.

После этого OkHttp должен начать создавать кэш ответа. Можно проверить каталог cache с помощью Android Studio Device Explorer.

Каталог кэша OkHttp
  • Каталог кэша должен находиться внутри каталога AppData data/data/$com_package_name/$cache_dir. В данном случае это должно иметь такой вид: data/data/com.trend.now/files/news_cache.
  • Как видно на приведенном выше изображении, внутри каталога news_cache есть несколько файлов — это кэш ответов, созданный OkHtpp.

Все ли готово?

Нет, не все. Кэшированный ответ есть, но OkHtpp не будет его использовать, если сервер не поддерживает его.

Как определить, поддерживает ли сервер кэшированные ответы?

По заголовку ответа с Cache-Control. Проверим заголовок ответа, возвращенного с rapidapi.com, с помощью Android Studio Network Inspector.

Ответ от rapidapi.com без Cache-Control
  • Как видите, в заголовке ответа (правое поле) отсутствует Cache-Control.

Это означает, что кэш есть, но любой запрос, сделанный с помощью Retrofit, все равно обращается к серверу, чтобы получить сетевой ответ. Если сервер поддерживает эту функцию, то заголовок ответа должен выглядеть так, как показано на приведенном ниже изображении.

Ответ с Cache-Control

Когда в заголовке ответа есть Cache-ControlOkHttp автоматически использует кэш, если он еще действителен, и запрашивает из сети, если срок его действия истек.

Как заставить OkHttp использовать кэшированные ответы

В качестве обходного пути можно заставить OkHttp использовать кэш, когда он доступен.

Сначала надо изменить OkHttp Cache на синглтон:

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

private const val CACHE_DIR = "news_cache"
private const val CACHE_SIZE_IN_MB = 50L // 50MB

@Provides
@Singleton
fun provideOkHttpClient(
cache: Cache // сюда - внедрение OkHttp Cache
): OkHttpClient {
return OkHttpClient.Builder()
// установка OkHttp Cache
.cache(cache)
...
}

@Provides
@Singleton
fun provideOkHttpCache(@ApplicationContext context: Context): Cache {
// определить путь каталога кэша
val file = File(context.filesDir, CACHE_DIR)
// установка каталога и размера кэша
return Cache(file, CACHE_SIZE_IN_MB * 1024 * 1024)
}

...
}

Теперь можно создать новый перехватчик (interceptor) OkHtpp для перехвата запроса.

class CacheInterceptor @Inject constructor(
private val cache: Cache // inject the okhttp cache
) : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val requestBuilder = request.newBuilder()
val url = request.url.toString()

// проверьте, доступен ли кэш ответов или нет
val responseCache = cache.urls().find { it == url }

val newRequest = if (responseCache != null) {
requestBuilder
// если он доступен, нужно заставить okhttp использовать кэшированный ответ
.cacheControl(CacheControl.FORCE_CACHE)
.build()
} else {
// Если кэш недоступен, действуйте как обычно
// обращение к серверу
requestBuilder.build()
}
return chain.proceed(newRequest)
}
}
  • Проверяю, есть ли в OkHttp кэшированный ответ от определенного url или нет, используя cache.urls(). Если кэш доступен — заставляю OkHttp использовать его с помощью CacheControl.FORCE_CACHE. Если нет — запрашиваю сервер, как обычно.
  • cache.urls() возвращает объект MutableIterator<String>, поиск строки в нем имеет сложность O(n). Большое количество кэшей может повлиять на производительность приложения и увеличить сетевую задержку. Но это — единственное из доступных мне средств, поэтому я им воспользовался.

Перехватчик кэша создается в файле CacheInterceptor.kt в каталоге core/network/.

core/
├── network/
│ ├── CacheInterceptor.kt
│ ├── HeaderInterceptor.kt

Наконец, регистрирую перехватчик в OkHtpp внутри Hilt-модуля.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

...

@Provides
@Singleton
fun provideOkHttpClient(cache: Cache): OkHttpClient {
return OkHttpClient.Builder()
...
// здесь происходит регистрация перехватчика
.addInterceptor(CacheInterceptor(cache))
...
}

...
}

Проверка ответа Retrofit

После всех вышеописанных изменений Retrofit должен использовать кэшированный ответ, если он доступен. Чтобы проверить это, добавлю точку останова в NewsRemoteDataSource, когда приложение вызовет fetchTrendingNews() во второй раз.

Точка останова fetchTrendingNews() в NewsRemoteDataSource
Необработанный ответ fetchTrendingNews()

При оценке response.raw() выясняется: cacheResponse не равен null, а networkResponse равен null. Это означает, что ответ получен из кэша.

Проблема решена?

Частично. В текущей реализации OkHtpp всегда использует кэш, когда он доступен, и никогда не отправит сетевой запрос, пока есть кэш. Я решил на время оставить эту реализацию с упомянутой выше проблемой.

Итоги реализации OkHttp Cache

  • Реализация OkHttp Cache для трендовых новостей поможет минимизировать ограничение скорости API.
  • Хотя кэш уже реализован, не все проблемы, связанные с ним, решены. Нужно еще определить, когда использовать кэш и делать сетевой запрос.
  • Важное замечание: возможно, вам придется пересмотреть использование OkHttp Cache, если сервер возвращает ответ с конфиденциальной информацией. Насколько я знаю, нет способа заставить OkHttp кэшировать только определенный url-ответ.

В следующей статье расскажу, как реализовал более эффективный механизм кэширования, объединив OkHttp Cache и базу данных Room, чтобы решить вышеуказанную проблему.

Ознакомьтесь с полным исходным кодом на Github

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Dani Mahardhika: Implement OkHttp Cache for Client-Side Caching

Предыдущая статьяC++: подробное руководство по размерам векторов
Следующая статья10 практик написания кода, на которые полагаются все старшие разработчики