В этом уроке обсудим, в каких потоках выполняется код загрузки данных. Разберем возможности LivePagedListBuilder. Узнаем, как использовать Paging Library с Room.
Полный список уроков курса:
- Урок 1. Lifecycle
- Урок 2. LiveData
- Урок 3. LiveData. Дополнительные возможности
- Урок 4. ViewModel
- Урок 5. Room. Основы
- Урок 6. Room. Entity
- Урок 7. Room. Insert, Update, Delete, Transaction
- Урок 8. Room. Query
- Урок 9. Room. RxJava
- Урок 10. Room. Запрос из нескольких таблиц. Relation
- Урок 11. Room. Type converter
- Урок 12. Room. Миграция версий базы данных
- Урок 13. Room. Тестирование
- Урок 14. Paging Library. Основы
- Урок 15. Paging Library. PagedList и DataSource. Placeholders.
- Урок 16. Paging Library. LivePagedListBuilder. BoundaryCallback.
- Урок 17. Paging Library. Виды DataSource
- Урок 18. Android Data Binding. Основы
- Урок 19. Android Data Binding. Код в layout. Доступ к View
- Урок 20. Android Data Binding. Обработка событий
- Урок 21. Android Data Binding. Observable поля. Двусторонний биндинг.
- Урок 22. Android Data Binding. Adapter. Conversion.
- Урок 23. Android Data Binding. Использование с include, ViewStub и RecyclerView.
- Урок 24. Navigation Architecture Component. Введение
- Урок 25. Navigation. Передача данных. Type-safe аргументы.
- Урок 26. Navigation. Параметры навигации
- Урок 27. Navigation. NavigationUI.
- Урок 28. Navigation. Вложенный граф. Global Action. Deep Link.
- Урок 29. WorkManager. Введение
- Урок 30. WorkManager. Критерии запуска задачи.
- Урок 31. WorkManager. Последовательность выполнения задач.
- Урок 32. WorkManager. Передача и получение данных
- Урок 33. Практика. О чем это будет.
- Урок 34. Практика. TodoApp. Список задач.
- Урок 35. Практика. TodoApp. Просмотр задачи
Потоки
Давайте поговорим про потоки, в которых происходит вся работа по загрузке данных. Все-таки PagedList с помощью DataSource тащит данные из базы данных или сервера. Эти операции могут занять какое-то время и должны выполняться в отдельном потоке.
Если вы помните, мы передаем пару Executor в билдер PagedList: setBackgroundThreadExecutor и setMainThreadExecutor. Первый используется как раз для выполнения DataSource методов, в которых мы получаем данные от Storage. А второй - для отправки полученных и обработанных данных в основной поток, чтобы обновлять RecyclerView.
Вроде как все ок. Но есть один нюанс. Не вся загрузка данных в DataSource методах будет происходить в BackgroundThreadExecutor. Первоначальная загрузка данных (метод loadInitial) будет выполнена в том потоке, где был создан PagedList. Если быть более точным, то она будет выполнена во время выполнения метода build билдера PagedList.Builder. И т.к. мы этот билдер вызывали в основном потоке, то и первоначальная загрузка будет выполнена в основном потоке.
LivePagedListBuilder
Для решения этой проблемы нам предлагается использовать LivePagedListBuilder. Он будет создавать PagedList в отдельном потоке, а, значит, и начальная загрузка данных будет выполнена в том же отдельном потоке. И когда все будет готово, он вернет нам готовый PgedList с начальными данными в основной поток, чтобы мы могли передать его в адаптер.
Чтобы LivePagedListBuilder мог создать PagedList, нам необходимо предоставить ему примерно то же самое, что мы используем при ручном создании PagedList: DataSource, PagedList.Config и BackgroundThreadExecutor. Только вместо DataSource надо будет передавать DataSource.Factory - фабрику, которую LivePagedListBuilder сможет использовать, чтобы самостоятельно создавать DataSource.
DataSource фабрика:
class MySourceFactory implements DataSource.Factory<Integer, Employee> { private final EmployeeStorage employeeStorage; MySourceFactory(EmployeeStorage employeeStorage) { this.employeeStorage = employeeStorage; } @Override public DataSource create() { return new MyPositionalDataSource(employeeStorage); } }
Используя эту фабрику LivePagedListBuilder всегда сам сможет создать новый MyPositionalDataSource.
Полный код создания всех объектов будет выглядеть немного иначе, чем раньше:
// DataSource MySourceFactory sourceFactory = new MySourceFactory(new EmployeeStorage()); // PagedList PagedList.Config config = new PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(10) .build(); LiveData<PagedList<Employee>> pagedListLiveData = new LivePagedListBuilder<>(sourceFactory, config) .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) .build(); // Adapter adapter = new EmployeeAdapter(diffUtilCallback); pagedListLiveData.observe(this, new Observer<PagedList<Employee>>() { @Override public void onChanged(@Nullable PagedList<Employee> employees) { Log.d(TAG, "submit PagedList"); adapter.submitList(employees); } }); // RecyclerView recyclerView.setAdapter(adapter);
Создаем SourceFactory и PagedList.Config и используем их в LivePagedListBuilder для создания LiveData<PagedList>. Создаем адаптер. Затем подписываемся на LiveData<PagedList>, который создаст PagedList, выполнит для него первоначальную загрузку и вернет нам в методе onChanged. Нам останется только передать его в submitList метод адаптера.
LivePagedListBuilder не просит у нас MainThreadExecutor, потому что у него есть свой.
У LivePagedListBuilder есть метод setInitialLoadKey. Он делает то же самое, что и аналогичный метод у PagedList.Builder - задает начальное значение для первоначальной погрузки. Но так как доступа к PagedList.Builder у нас теперь нет, то следует использовать этот метод.
invalidate
Презентация с Google IO.
Может возникнуть ситуация, когда в Storage обновятся данные, которые мы уже подгрузили и отображаем. Например, PagedList уже подтянул 80 записей, как внезапно в Storage поменялась запись на позиции 30. Чтобы обновить эту запись в списке, нам необходимо создавать новую пару PagedList + DataSource и загружать все снова.
LivePagedListBuilder умеет это делать автоматически. Это будет выглядеть так:
1) В DataSource нам необходимо понять, что данные в Storage обновились. Как мы это сделаем - зависит от реализации Storage. Либо мы подписываем какой-то специальный слушатель на Storage, либо с очередной порцией данных мы получим от Storage какой-нибудь invalidate флаг.
2) Как только мы понимаем, что данные в Storage обновились, нам в DataSource необходимо вызвать метод invalidate.
3) LivePagedListBuilder поймет, что надо пересоздавать пару PagedList + DataSource. У него есть для этого все необходимые инструменты (PagedList.Config и Source.Factory), поэтому он быстро создает новую пару.
4) При создании, новый PagedList сразу подгрузит первоначальную порцию данных. Подгружать он будет не с нулевой позиции, а с той, которая сейчас открыта в списке, чтобы список не скроллить в начало. При последующих скроллах списка PagedList будет подгружать остальные данные.
Если данные тянутся из базы данных, то пользователь даже не заметит, что список заново грузит все данные. А вот если данные идут с сервера, то тут уже пользователь может обратить внимание, что список подгружает данные, которые ранее (до invalidate) уже были загружены.
setBoundaryCallback
На LivePagedListBuilder мы можем повесить колбэк методом setBoundaryCallback. Этот колбэк способен сообщить нам о трех событиях:
- загружен самый первый в списке элемент, т.е. перед ним уже ничего не будет запрашиваться и подгружаться (onItemAtFrontLoaded)
- загружен самый последний в списке элемент, т.е. после него уже ничего не будет запрашиваться и подгружаться (onItemAtEndLoaded)
- в первоначальной загрузке ни пришло никаких данных, т.е. данных не будет вообще (onZeroItemsLoaded)
Это может быть полезно, если нам, например, надо показать данные из базы данных, которая является кэшем для данных из инета. Мы тянем данные из базы данных, пока они там не закончатся. О том, что данные в БД закончились, мы узнаем, т.к. будет вызван метод onItemAtEndLoaded в BoundaryCallback. Мы дергаем сервак, берем у него новую порцию данных и пишем их в БД. После этого в DataSource надо вызвать invalidate, чтобы LivePagedListBuilder создал новую пару PagedList + DataSource. Эта новая пара загрузит новые данные.
Важно понимать, что вызов invalidate тут обязателен. Потому что иначе PagedLIst будет считать, что он уже загрузил все данные и не будет больше запрашивать данные у DataSource.
Также метод setBoundaryCallback есть и у PagedList.Builder.
Room
Если вы используете Room, то Dao может предоставить вам DataSource.Factory
@Dao public interface EmployeeDao { @Query("SELECT * FROM employee") DataSource.Factory<Integer, Employee> getAll(); ... }
DataSource, полученный из этой фабрики, сам умеет вызывать invalidate, когда данные в базе данных обновляются.
Вам остается только передать эту фабрику в LivePagedListBuilder.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня