В предыдущей статье я описал процесс реализации механизма кэширования новостных тем в приложении 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 не будет его использовать, если сервер не поддерживает его.

Telegram-канал по теме
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться

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

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

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

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

Ответ с Cache-Control

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

Как заставить 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. 

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


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

Мы в Telegram — подписывайтесь
NOP::Nuances of ProgrammingСтатьи и переводы для разработчиков — каждый день
Подписаться
NOP::HumorИТ-юмор и мемы о боли разработчиков
Подписаться
NOP::Recruiter RemoteУдалённые ИТ-вакансии с вилками
Подписаться
Предыдущая статьяC++: подробное руководство по размерам векторов
Следующая статья10 практик написания кода, на которые полагаются все старшие разработчики