Кэшируем пагинацию в 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.
На этом все. Если вы знаете другие способы как “закэшировать” пагинацию, то обязательно напишите в комменты.
Источник
Top 10: Best Android Image Loading and Caching Libraries
Carlos Delgado
See our review from 10 of the Best Android Image Loading and Caching Libraries.
An application that doesn’t load images, is pretty weird and boring nowadays. Knowing how to display an image in your mobile application is one of the most common tasks for every developer. However, the way you work with them will be different for everyone, due to the way you code your app to handle the image. For example, website developers don’t need to take care of image caching because the browser does this automatically, but for an android developer, an image will be normally loaded again and again (if we are talking from a remote/web source) without really good performance. If you are willing to display efficiently images on your application, you need to take care of the image caching, specially when you work on a free application to organize photos.
In this top, we’ll share with you 10 of the most imponent image caching libraries to increase the performance and loading times of your Android Application.
10. Mirage
Mirage is an image loading library developed by the Android team at The Climate Corporation for loading, caching, and sync’ing for offline usage of images. Our main consideration for creating this system was to allow for explicit sync’ing of images for offline use. Libraries like Picasso didn’t fulfill our requirements because as stated «Picasso doesn’t have a disk cache. It delegates to whatever HTTP client».
9. Android Image Cache
An image download-and-cacher that also knows how to efficiently generate and retrieve thumbnails of various sizes. This library features:
- easily integrates into content-provider backed applications, providing an adapter that can read local and web URLs from a cursor
- automatic generation and caching of multiple sizes of images based on one downloaded asset
- provides a disk cache as well as a memory cache
- automatic disk cache management; no setup necessary, but parameters can be fine-tuned if desired
- designed to work with your existing setup: no extending a custom application or activity needed
- cursor adapter supports multiple image fields for each ImageView; skips fields that are null or empty
- cursor adapter has an automatic progress bar when loading the cursor
8. Shutterbug
Shutterbug is an Android library that lets you fetch remote images and cache them. It is particularly suited for displaying remote images in lists or grids as it includes a convenience subclass of ImageView ( FetchableImageView ) that make implementation a one-liner.
A dual memory and disk cache was implemented. It makes use of two backports of Android classes: LruCache for the memory part and DiskLruCache for the disk part. LruCache was introduced by API Level 12, but we provide it here as a standalone class so you can use the library under lower level APIs. Both LruCache and DiskLruCache are licensed under the Apache Software License, 2.0.
Shutterbug was inspired by SDWebImage which does the same thing on iOS. It uses the same structure and interface. People who are familiar with SDWebImage on iOS will feel at home with Shutterbug on Android.
7. Slight
Sligh is an easy, sample and flexible library for loading, caching and displaying images on Android written in Kotlin.
6. Ion
ION is an Android Asynchronous Networking and Image Loading library. This library features:
- Asynchronously download:
- Images into ImageViews or Bitmaps (animated GIFs supported too)
- JSON (via Gson)
- Strings
- Files
- Java types using Gson
- Easy to use Fluent API designed for Android
- Automatically cancels operations when the calling Activity finishes
- Manages invocation back onto the UI thread
- All operations return a Future and can be cancelled
- HTTP POST/PUT:
- text/plain
- application/json — both JsonObject and POJO
- application/x-www-form-urlencoded
- multipart/form-data
- Transparent usage of HTTP features and optimizations:
- SPDY and HTTP/2
- Caching
- Gzip/Deflate Compression
- Connection pooling/reuse via HTTP Connection: keep-alive
- Uses the best/stablest connection from a server if it has multiple IP addresses
- Cookies
- View received headers
- Grouping and cancellation of requests
- Download progress callbacks
- Supports file:/, http(s):/, and content:/ URIs
- Request level logging and profiling
- Support for proxy servers like Charles Proxy to do request analysis
- Based on NIO and AndroidAsync
- Ability to use self signed SSL certificates
5. Android Smart Image View
SmartImageView is a drop-in replacement for Android’s standard ImageView which additionally allows images to be loaded from URLs or the user’s contact address book. Images are cached to memory and to disk for super fast loading. This library features:
- Drop-in replacement for ImageView
- Load images from a URL
- Load images from the phone’s contact address book
- Asynchronous loading of images, loading happens outside the UI thread
- Images are cached to memory and to disk for super fast loading
- SmartImage class is easily extendable to load from other sources
4. Android Universal Image Loader
UIL is a powerful and flexible library for loading, caching and displaying images on Android. UIL aims to provide a powerful, flexible and highly customizable instrument for image loading, caching and displaying. It provides a lot of configuration options and good control over the image loading and caching process. This library features:
- Multithread image loading (async or sync)
- Wide customization of ImageLoader’s configuration (thread executors, downloader, decoder, memory and disk cache, display image options, etc.)
- Many customization options for every display image call (stub images, caching switch, decoding options, Bitmap processing and displaying, etc.)
- Image caching in memory and/or on disk (device’s file system or SD card)
- Listening loading process (including downloading progress)
- Android 2.0+ support.
3. Fresco by Facebook
An Android library for managing images and the memory they use. Fresco takes care of image loading and display, so you don’t have to. It will load images from the network, local storage, or local resources, and display a placeholder until the image has arrived. It has two levels of cache; one in memory and another in internal storage. In Android 4.x and lower, Fresco puts images in a special region of Android memory. This lets your application run faster — and suffer the dreaded OutOfMemoryError much less often. Fresco also supports:
- streaming of progressive JPEGs
- display of animated GIFs and WebPs
- extensive customization of image loading and display
- and much more!
2. Picasso
Picasso is a powerful image downloading and caching library for Android. Images add much-needed context and visual flair to Android applications. Picasso allows for hassle-free image loading in your application—often in one line of code! Many common pitfalls of image loading on Android are handled automatically by Picasso:
- Handling ImageView recycling and download cancelation in an adapter.
- Complex image transformations with minimal memory use.
- Automatic memory and disk caching.
Adapter re-use is automatically detected and the previous download canceled. If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request.
1. Glide
Glide is a fast and efficient open source media management and image loading framework for Android that wraps media decoding, memory and disk caching, and resource pooling into a simple and easy to use interface. Glide supports fetching, decoding, and displaying video stills, images, and animated GIFs. Glide includes a flexible API that allows developers to plug in to almost any network stack. By default Glide uses a custom HttpUrlConnection based stack, but also includes utility libraries plug in to Google’s Volley project or Square’s OkHttp library instead.
Glide’s primary focus is on making scrolling any kind of a list of images as smooth and fast as possible, but Glide is also effective for almost any case where you need to fetch, resize, and display a remote image. Glide takes in to account two key aspects of image loading performance on Android:
- The speed at which images can be decoded.
- The amount of jank incurred while decoding images.
For users to have a great experience with an app, images must not only appear quickly, but they must also do so without causing lots of jank and stuttering from main thread I/O or excessive garbage collections.
Glide takes a number of steps to ensure image loading is both as fast and as smooth as possible on Android:
- Smart and automatic downsampling and caching minimize storage overhead and decode times.
- Aggressive re-use of resources like byte arrays and Bitmaps minimizes expensive garbage collections and heap fragmentation.
- Deep lifecycle integration ensures that only requests for active Fragments and Activities are prioritized and that Applications release resources when neccessary to avoid being killed when backgrounded.
Honorable mentions
An image loading library for Android backed by Kotlin Coroutines. Coil is:
- Fast: Coil performs a number of optimizations including memory and disk caching, downsampling the image in memory, re-using Bitmaps, automatically pausing/cancelling requests, and more.
- Lightweight: Coil adds
1500 methods to your APK (for apps that already use OkHttp and Coroutines), which is comparable to Picasso and significantly less than Glide and Fresco.
Coil is an acronym for: Coroutine Image Loader.
If you know another awesome image caching library for Android, please share it with the community in the comment box.
Источник