Retrofit 2: Http Caching in Android
May 28, 2020 · 3 min read
Caching is nothing but a way to store network fetched data on a device’s storage and access later when the device is offline or we want the same data again and again. Some image loading library like Picasso or Glide provide this caching when loading and displaying images but retrofit does not use this by default for its requests.
So in this article, you’ll learn the basics of caching and the way to enable online and offline caching in your Android app with Retrofit and Okhttp libraries.
Before starting lets know why caching is important…
Advantages of Caching
- It reduces bandwidth consumption; therefore, it decreases network traffic and diminishes network congestion.
- Saves you time you’d spend waiting for the server to get response.
- If you need to access the same network resource again after having accessed it recently, your device won’t need to make a request to the server; it’ll get the cached response instead.
So Let’s start caching requests
Befor e starting, we will go through some important terms which are related to caching like: Okhttp, Interceptor, Cache-control, max-age, max-stale.
* Okhttp
This is the default HttpClient for Retrofit.
* Interceptors
Interceptor is a powerful component of this okhttp through which we can read and modify the requests and obviously, we will use this interceptor for our Cache control. There are two types of interceptor:
- Application Interceptors — Gets you the final response.
- Network Interceptors — To intercept intermediate requests.
For caching, we will use Network interceptor.
* Cache-control
Cache-control is an header used to specify caching policies in client requests and server responses. Inside the interceptors, we will to get the current request using chain.request() and add caching options to that request.
By the following way, we can add a “Cache-Control” header to the request: «public, only-if-cached, max-stale=60»
Then do a chain.proceed(request) to proceed with the modified request to return the response.
Find details for cache-control here.
* max-age =
max-age is the oldest limit ( lower limit) till which the response can be returned from the cache. It takes value in seconds.
* max-stale =
max-stale is the highest limit beyond which cache cannot be returned. It also takes value in seconds.
Step 1: Define a method to check for internet connectivity
We first need to have a method in our app that checks for Internet connectivity. May be you’re already familiar with it and want to take a look at it again:
Источник
Кэшируем пагинацию в Android
Наверняка каждый Android разработчик работал со списками, используя RecyclerView. А также многие успели посмотреть как организовать пагинацию в списке, используя Paging Library из Android Architecture Components.
Все просто: устанавливаем PositionalDataSource, задаем конфиги, создаем PagedList и скармливаем все это вместе с адаптером и DiffUtilCallback нашему RecyclerView.
Но что если у нас несколько источников данных? Например, мы хотим иметь кэш в Room и получать данные из сети.
Кейс получается довольно кастомный и в интернете не так уж много информации на эту тему. Я постараюсь это исправить и показать как можно решить такой кейс.
Если вы все еще не знакомы с реализацией пагинации с одним источником данных, то советую перед чтением статьи ознакомиться с этим.
Как бы выглядело решение без пагинации:
- Обращение к кэшу (в нашем случае это БД)
- Если кэш пуст — отправка запроса на сервер
- Получаем данные с сервера
- Отображаем их в листе
- Пишем в кэш
- Если кэш имеется — отображаем его в списке
- Получаем актуальные данные с сервера
- Отображаем их в списке○
- Пишем в кэш
Такая удобная штука как пагинация, которая упрощает жизнь пользователям, тут нам ее усложняет. Давайте попробуем представить какие проблемы могут возникнуть при реализации пагинируемого списка с несколькими источниками данных.
Алгоритм примерно следующий:
- Получаем данные из кэша для первой страницы
- Если кэш пуст — получаем данные сервера, отображаем их в списке и пишем в БД
- Если кэш есть — загружаем его в список
- Если доходим до конца БД, то запрашиваем данные с сервера, отображаем их
- в списке и пишем в БД
Из особенностей такого подхода можно заметить, что для отображения списка в первую очередь опрашивается кэш, и сигналом загрузки новых данных является конец кэша.
В Google задумались над этим и создали решение, которое идет из коробки PagingLibrary — BoundaryCallback.
BoundaryCallback сообщает когда локальный источник данных “заканчивается” и уведомляет об этом репозиторий для загрузки новых данных.
На официальном сайте Android Dev есть ссылка на репозиторий с примером проекта, использующего список с пагинацией с двумя источниками данных: Network (Retrofit 2) + Database (Room). Для того, чтобы лучше понять как работает такая система попробуем разобрать этот пример, немного его упростим.
Начнем со слоя data. Создадим два DataSource.
В этом интерфейсе описаны запросы к API Reddit и классы модели (ListingResponse, ListingData, RedditChildrenResponse), в объекты которых будут сворачиваться ответы API.
И сразу сделаем модель для Retrofit и Room
Класс RedditDb.kt, который будет наследовать RoomDatabase.
Помним, что создавать класс RoomDatabase каждый раз для выполнения запроса к БД очень затратно, поэтому в реальном кейсе создавайте его единожды за все время жизни приложения!
И класс Dao с запросами к БД RedditPostDao.kt
Вы наверное заметили, что метод получения записей postsBySubreddit возвращает
DataSource.Factory. Это необходимо для создания нашего PagedList, используя
LivePagedListBuilder, в фоновом потоке. Подробнее об этом вы можете почитать в
уроке.
Отлично, слой data готов. Переходим к слою бизнес логики.Для реализации паттерна “Репозиторий” принято создавать интерфейс репозитория отдельно от его реализации. Поэтому создадим интерфейс RedditPostRepository.kt
И сразу вопрос — что за Listing? Это дата класс, необходимый для отображения списка.
Создаем реализацию репозитория MainRepository.kt
Давайте посмотрим что происходит в нашем репозитории.
Создаем инстансы наших датасорсов и интерфейсы доступа к данным. Для базы данных:
RoomDatabase и Dao, для сети: Retrofit и интерфейс апи.
Далее реализуем обязательный метод репозитория
который настраивает пагинацию:
- Создаем SubRedditBoundaryCallback, наследующий PagedList.BoundaryCallback<>
- Используем конструктор с параметрами и передадим все, что нужно для работы BoundaryCallback
- Создаем триггер refreshTrigger для уведомления репозитория о необходимости обновить данные
- Создаем и возвращаем Listing объект
В Listing объекте:
- livePagedList
- networkState — состояние сети
- retry — callback для вызова повторного получения данных с сервера
- refresh — тригер для обновления данных
- refreshState — состояние процесса обновления
Реализуем вспомогательный метод
для записи ответа сети в БД. Он будет использоваться, когда нужно будет обновить список или записать новую порцию данных.
Реализуем вспомогательный метод
для тригера обновления данных. Тут все довольно просто: получаем данные с сервера, чистим БД, записываем новые данные в БД.
С репозиторием разобрались. Теперь давайте взглянем поближе на SubredditBoundaryCallback.
В классе, который наследует BoundaryCallback есть несколько обязательных методов:
Метод вызывается, когда БД пуста, здесь мы должны выполнить запрос на сервер для получения первой страницы.
Метод вызывается, когда “итератор” дошел до “дна” БД, здесь мы должны выполнить запрос на сервер для получения следующей страницы, передав ключ, с помощью которого сервер выдаст данные, следующие сразу за последней записью локального стора.
Метод вызывается, когда “итератор” дошел до первого элемента нашего стора. Для реализации нашего кейса можем проигнорировать реализацию этого метода.
Дописываем колбэк для получения данных и передачи их дальше
Дописываем метод записи полученных данных в БД
Что за хэлпер PagingRequestHelper? Это ЗДОРОВЕННЫЙ класс, который нам любезно предоставил Google и предлагает вынести его в библиотеку, но мы просто скопируем его в пакет слоя логики.
* The helper provides an API to observe combined request status, which can be reported back to the * application based on your business rules. * */ // THIS class is likely to be moved into the library in a future release. Feel free to copy it // from this sample. public class PagingRequestHelper < private final Object mLock = new Object(); private final Executor mRetryService; @GuardedBy("mLock") private final RequestQueue[] mRequestQueues = new RequestQueue[]
mListeners = new CopyOnWriteArrayList<>(); /** * Creates a new com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper with the given <@link Executor>which is used to run * retry actions. * * @param retryService The <@link Executor>that can run the retry actions. */ public PagingRequestHelper(@NonNull Executor retryService) < mRetryService = retryService; >/** * Adds a new listener that will be notified when any request changes <@link Status state>. * * @param listener The listener that will be notified each time a request’s status changes. * @return True if it is added, false otherwise (e.g. it already exists in the list). */ @AnyThread public boolean addListener(@NonNull Listener listener) < return mListeners.add(listener); >/** * Removes the given listener from the listeners list. * * @param listener The listener that will be removed. * @return True if the listener is removed, false otherwise (e.g. it never existed) */ public boolean removeListener(@NonNull Listener listener) < return mListeners.remove(listener); >/** * Runs the given <@link Request>if no other requests in the given request type is already * running. *
* If run, the request will be run in the current thread. * * @param type The type of the request. * @param request The request to run. * @return True if the request is run, false otherwise. */ @SuppressWarnings(«WeakerAccess») @AnyThread public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) < boolean hasListeners = !mListeners.isEmpty(); StatusReport report = null; synchronized (mLock) < RequestQueue queue = mRequestQueues[type.ordinal()]; if (queue.mRunning != null) < return false; >queue.mRunning = request; queue.mStatus = Status.RUNNING; queue.mFailed = null; queue.mLastError = null; if (hasListeners) < report = prepareStatusReportLocked(); >> if (report != null) < dispatchReport(report); >final RequestWrapper wrapper = new RequestWrapper(request, this, type); wrapper.run(); return true; > @GuardedBy(«mLock») private StatusReport prepareStatusReportLocked() < Throwable[] errors = new Throwable[]< mRequestQueues[0].mLastError, mRequestQueues[1].mLastError, mRequestQueues[2].mLastError >; return new StatusReport( getStatusForLocked(RequestType.INITIAL), getStatusForLocked(RequestType.BEFORE), getStatusForLocked(RequestType.AFTER), errors ); > @GuardedBy(«mLock») private Status getStatusForLocked(RequestType type) < return mRequestQueues[type.ordinal()].mStatus; >@AnyThread @VisibleForTesting void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) < StatusReport report = null; final boolean success = throwable == null; boolean hasListeners = !mListeners.isEmpty(); synchronized (mLock) < RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()]; queue.mRunning = null; queue.mLastError = throwable; if (success) < queue.mFailed = null; queue.mStatus = Status.SUCCESS; >else < queue.mFailed = wrapper; queue.mStatus = Status.FAILED; >if (hasListeners) < report = prepareStatusReportLocked(); >> if (report != null) < dispatchReport(report); >> private void dispatchReport(StatusReport report) < for (Listener listener : mListeners) < listener.onStatusChange(report); >> /** * Retries all failed requests. * * @return True if any request is retried, false otherwise. */ public boolean retryAllFailed() < final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length]; boolean retried = false; synchronized (mLock) < for (int i = 0; i
Со слоем бизнес логики закончили, можем переходить к реализации представления.
В слое представления у нас новая MVVM от Google на ViewModel и LiveData.
В методе onCreate инициализируем ViewModel, адаптер списка, подписываемся на изменение названия подписки и вызываем через модель запуск работы репозитория.
Если вы не знакомы с механизмами LiveData и ViewModel, то рекомендую ознакомиться с уроками.
В модели реализуем методы, которые будут дергать методы репозитория: retry и refesh.
Адаптер списка будет наследовать PagedListAdapter. Тут все также как и работе с пагинацией и одним источником данных.
И все те же ViewHolder ы для отображения записи и итема состояния загрузки данных из сети.
Если мы запустим приложение, то можем увидеть прогресс бар, а затем и данные с Reddit по запросу androiddev. Если отключим сеть и долистаем до конца нашего списка, то будет сообщение об ошибке и предложение попытаться загрузить данные снова.
Все работает, супер!
И мой репозиторий, где я постарался немного упростить пример от Google.
На этом все. Если вы знаете другие способы как “закэшировать” пагинацию, то обязательно напишите в комменты.
Источник
Implement Caching In Android Using RxJava Operators
First, we need to understand why caching is useful? Caching is very useful in the following situations:
- Reduce network calls: We can reduce the network calls by caching the network response.
- Fetch the data very fast: We can fetch the data very fast if it is cached.
There are two types of caching as follows:
- Memory Cache: It keeps the data in the memory of the application. If the application gets killed, the data is evicted. Useful only in the same session of application usage. Memory cache is the fastest cache to get the data as this is in memory.
- Disk Cache: It saves the data to the disk. If the application gets killed, the data is retained. Useful even after the application restarts. Slower than memory cache, as this is I/O operation.
How caching works?
The first time, the user opens the app, there will be no data in memory and no data in the disk cache. So the application will have to make a network call to fetch the data. It will fetch the data from the network and save it to the disk, keep it in the memory cache and return the data.
If the user goes to the same screen in the same session, the data will be fetched very fast from the memory cache.
If the user kills the app and restarts, it will fetch the data from the disk cache and keep it in the memory cache and return the data.
As every data comes with validity, the cache will only return the data if the data is valid at the time of retrieving.
Now, we have understood how caching works.
Let’s start with the implementation part using the RxJava Operators.
For example, Data model class
Class to simulate memory data source
Class to simulate disk data Source
Class to simulate network data Source
The DataSource to handle 3 data sources —
Now using the concat and firstElement operator
Here, we have used the concat operator to maintain the order of observables which is first check in memory, then in disk and finally in network.
So the concat operator will help us to maintain the order.
Next Rxjava operator is firstElement, this operator will make sure if we get the data from memory, it will not let the other observables(disk, network) do anything or if we get the data from the disk, it will not let the network observable do anything. So, this way no redundant work at all. This is how firstElement operator will help.
This way we can implement the caching in Android applications using the RxJava Operators.
Источник