#android #paging #flow #android-paging #android-paging-library
#Android #android-подкачка #android-библиотека подкачки
Вопрос:
Я использую paging3
, и у меня есть два разных источника подкачки. Проблема заключается Coroutine Scope
только в том, что генерируется первый поток подкачки
У ViewModel
меня есть два потока подкачки
val pagingFlow1 = Pager(PagingConfig(pageSize = 50, prefetchDistance = 1)) {
pagingSource
}.flow.cachedIn(viewModelScope)
val pagingFlow2 = Pager(PagingConfig(pageSize = 50, prefetchDistance = 1)) {
pagingSource2
}.flow.cachedIn(viewModelScope)
Соберите их в activity
lifecycleScope.launch(Dispatchers.IO) {
viewModel.pagingFlow1.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData)
}
viewModel.pagingFlow2.collectLatest { pagingData ->
pagingAdapter2.submitData(pagingData)
}
}
Но lifecycleScope
только испускать pagingFlow1
, другими словами, подкачка работает только в первом RecyclerView.
Когда я меняю порядок, на этот раз работает только для pagingFlow2
lifecycleScope.launch(Dispatchers.IO) {
viewModel.pagingFlow2.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData)
}
viewModel.pagingFlow1.collectLatest { pagingData ->
pagingAdapter2.submitData(pagingData)
}
}
Чтобы убедиться, что я протестировал его с базовыми потоками и работает нормально
// Flows in ViewModel
val testFlow1 = flowOf(1,2,3)
val testFlow2 = flowOf(4,5,6)
// Activity
lifecycleScope.launch(Dispatchers.IO) {
viewModel.testFlow1.collectLatest { item ->
Log.d(item)
}
viewModel.testFlow2.collectLatest { item ->
Log.d(item)
}
}
Я не могу понять, почему при использовании подкачки выделяется только первый поток? Кто-нибудь даст мне подсказку?
Пробуя разные вещи, я обнаружил интересное поведение. Мы ничего не сможем собрать, если сначала соберем pagingFlow
val flow3 = flowOf(1,2,3)
lifecycleScope.launch(Dispatchers.IO) {
flow3.collectLatest { pagingData ->
LogUtils.d("PagingFlow3 $pagingData")
}
viewModel.pagingFlow1.collectLatest { pagingData ->
LogUtils.d("PagingFlow1 $pagingData")
pagingAdapter.submitData(pagingData)
}
viewModel.pagingFlow2.collectLatest { pagingData ->
LogUtils.d("PagingFlow2 $pagingData")
pagingAdapter2.submitData(pagingData)
}
}
Сначала flow3
собрано, чем pagingFlow1
собрано, но pagingFlow2
не собрано
Если мы поместим flow3 ниже pagingFlow1
, он не будет собран
val flow3 = flowOf(1,2,3)
lifecycleScope.launch(Dispatchers.IO) {
viewModel.pagingFlow1.collectLatest { pagingData ->
LogUtils.d("PagingFlow1 $pagingData")
pagingAdapter.submitData(pagingData)
}
flow3.collectLatest { pagingData ->
LogUtils.d("PagingFlow3 $pagingData")
}
viewModel.pagingFlow2.collectLatest { pagingData ->
LogUtils.d("PagingFlow2 $pagingData")
pagingAdapter2.submitData(pagingData)
}
}
Только pagingFlow1
собранный
Ответ №1:
collectLatest приостанавливается до завершения потока, поэтому вам необходимо запускать отдельные задания.
Кроме того, вам не нужно отправлять диспетчер ввода-вывода.
РЕДАКТИРОВАТЬ: некоторые изменения в подкачке с момента публикации этого ответа — больше не имеет значения, к какому диспетчеру вы обращаетесь .submitData
. Единственное, на что это влияет, — это, возможно, то, где происходят выделения при инициализации, и, возможно, вы хотите запустить их из потока, отличного от пользовательского интерфейса, но в целом это не повлияет на производительность.
например,
lifecycleScope.launch {
viewModel.pagingFlow1.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData) } }
lifecycleScope.launch {
viewModel.pagingFlow2.collectLatest { pagingData ->
pagingAdapter2.submitData(pagingData) } }
Комментарии:
1. Это работает, но почему он выдает ошибку, когда я добавляю диспетчер ввода-вывода? там написано java.lang. Исключение IllegalArgumentException: SavedStateProvider с данным ключом уже зарегистрирован @dlam
2. Хороший вопрос 🙂 Я не уверен, что у меня в голове (это может быть просто ошибка, поскольку мы «должны» переключиться на mainDispatcher внутренне), но submitData предназначен для вызова mainDispatcher, поскольку он предотвращает необходимость возврата к основному потоку, когда новые элементы наблюдаются и применяются к RecyclerView. Это диктует контекст для наблюдателя событий, который является всем материалом пользовательского интерфейса.
3. Привет @dlam, где мы можем найти внутреннее переключение, которое происходит при использовании Paging3?
4. @akubi что вы подразумеваете под «найти»? В общем, для приостановки сопрограмм вы можете просто позволить реализации решить, когда необходимо переключать контексты. На самом деле этот ответ немного устарел, поскольку технически может быть желательно разрешить выделение ресурсов при инициализации подкачки в фоновом потоке, и больше не имеет значения, в каком контексте вы вызываете
.submitData
. Я обновлю свои предыдущие ответы здесь.
Ответ №2:
-
Сначала вы должны понять, что вы извлекаете значение асинхронно всякий раз, когда используете потоки в своем коде. Это означает, что вызову .collect для вашей активности придется подождать, пока из потока не будет получено значение. Я предполагаю, что это происходит только при чтении из базы данных в вашем случае, что, если вы используете потоки в своем типе возвращаемого значения DAO, будет срабатывать только в том случае, если какая-либо функция изменяет поле базы данных. Это означает, что пока кто-то не изменит базу данных, не переходите к этой точке в коде (другими словами, она блокирует поток). Исправление этого выполняется как your .collect параллельно, а не последовательно. Это показано в ответе dlam, где он / она запускает их параллельно, и они работают.
-
Хорошо, так что его / ее ответ работает, так зачем нам тогда нужен этот пост? Ну, хотя это работает для пользователя, все эти операции выполняются в основном потоке, который в основном замораживает пользовательский интерфейс на определенное количество кадров, поскольку поток, который отвечает за обновление пользовательского интерфейса (Dispatchers.Main), занят выполнением ваших операций. Это можно увидеть в вашем logcat с сообщением:
Я / Хореограф: пропущено xAmountOf кадров! Возможно, приложение выполняет слишком много работы в своем основном потоке.
Чтобы избежать зависания пользовательского интерфейса, основной поток следует использовать только для обновления пользовательского интерфейса. Исправление для этого заключается в выполнении этих операций в фоновом потоке с использованием Dispatchers.IO
- Если вы дойдете до # 2, вы, вероятно, получите сообщение об ошибке:
java.lang.Исключение IllegalArgumentException: SavedStateProvider с данным ключом уже зарегистрирован
Это вызвано тем, что вы вызываете .cachedIn(ViewmodelScope) в ваших потоках. Этот метод кэширует выгружаемые данные в вашей области, чтобы вы могли быстрее получить к ним доступ в следующий раз, когда вам это понадобится, и он собирает мусор, когда область, в которую вы передали, завершается, ошибка в основном говорит о том, что вы пытались сохранить второй набор данных с тем же ключом, что и набор, который уже сохранен.
-
Я предполагаю, что эти ключи SavedStateProvider, вероятно, автоматически генерируются. Это означает, что какой бы .collect ни был запущен первым (помните, что теперь они работают параллельно, поэтому это может быть любой из них), он будет сохранен с помощью автоматически сгенерированного ключа, скажем, для простоты «1», что нормально, однако, когда запускается second .collect, и он переходит к сохранению своего SavedStateProvider дляпо какой-то причине также автоматически генерируется «1» в качестве ключа, который вызывает конфликт. Ключ к поиску решения этой проблемы — выяснить, что заставляет их автоматически генерировать один и тот же ключ.
-
Я думаю, что причина, по которой они генерируют один и тот же ключ, заключается в том, что они работают в параллельных потоках, а второй запускаемый генерирует ключ до того, как первый закончил его сохранение, поэтому второй не знает, что уже что-то с этим ключом кэшируется. Это подтверждается тем фактом, что в ответе dlam эта ошибка не появилась, почему бы и нет? скорее всего, что-то связано с тем, что главный диспетчер запускает их вместо диспетчера ввода-вывода.
-
На данный момент я бы сказал, что исправление заключается в том, чтобы убедиться, что ключ генерируется только один раз, но поскольку у нас нет доступа к коду, мы мало что можем сделать в этом отношении… Я бы сказал, что лучше всего удалить оператор .cachedIn из ваших потоков, тогда, предположительно, это сработает.
lifecycleScope.launch(Dispatchers.IO) { viewModel.pagingFlow1.collectLatest { pagingData -> LogUtils.d("PagingFlow1 $pagingData") pagingAdapter.submitData(pagingData) } } lifecycleScope.launch(Dispatchers.IO) { viewModel.pagingFlow2.collectLatest { pagingData -> LogUtils.d("PagingFlow2 $pagingData") pagingAdapter2.submitData(pagingData) } }
Комментарии:
1. По-прежнему выдает ту же ошибку при вводе диспетчера ввода-вывода, java.lang. Исключение IllegalArgumentException: SavedStateProvider с данным ключом уже зарегистрирован
2. Просто отметим для дополнительной ясности, .cachedIn — это просто многоадресная рассылка с буферизацией (с объединением событий), она не имеет ничего общего с сохраненным состоянием, хотя она используется в случае восстановления состояния.
3. Да, я провел дополнительные исследования, и в основном я был абсолютно неправ после моего # 3 проблема в соответствии с этим github.com/Kotlin/kotlinx.coroutines/issues/1933 потоки имеют вызов для запуска в фоновом потоке вместо того, чтобы охватывать их другим заданием, как это сделал я. «.launchIn()»