Status Update
Comments
an...@google.com <an...@google.com>
ap...@google.com <ap...@google.com> #2
Branch: androidx-main
commit 16278c75a09262bfc2b1e230730f79ec5a4d4918
Author: Andrey Kulikov <andreykulikov@google.com>
Date: Tue May 25 18:25:16 2021
Do not reset initial scroll position in LazyColumn/Row when items are loaded asynchronously
If the initial scroll position(or restored scroll position) is not 0 this value will be immediately overridden by 0 if there were no items yet provided for LazyColumn/Row.
It is easily reproducible with Paging where we start loading asynchronously and there is always a first frame when we don't have any items.
Fixes: 177245496
Test: new tests for LazyColumn and LazyRow
Change-Id: I95e374fb06dc480aab7b56b8b3cacb92ca1188a9
M compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
M compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowTest.kt
M compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
b9...@gmail.com <b9...@gmail.com> #3
an...@google.com <an...@google.com> #4
Yes, we basically just changed the default behavior for the empty case for now. Please track this bug
b9...@gmail.com <b9...@gmail.com> #5
My workaround currently is to keep `LazyPagingItems` object in `ViewModel`, so that `itemCount` and other state will be the same when navigated back.
an...@google.com <an...@google.com> #6
yes, it is expected as the paging is starting loading asynchronously and there is always at least one frame when there is no items yet
b9...@gmail.com <b9...@gmail.com> #7
an...@google.com <an...@google.com> #8
so we can discuss this separately.
Thanks!
da...@gmail.com <da...@gmail.com> #9
Where can I find the new bug? this issue affect me as well.
vi...@gmail.com <vi...@gmail.com> #10
Can you provide an example? In my case stored `LazyPagingItems` object does not want to load data
upd: No longer needed. In my case the issue with LazyVerticalGrid. For myself I found solution - I made copy of LazyPagingItems to make counstructor public and create LazyPagingItems inside ViewModel, so reloading does not occur when navigating backwards
[Deleted User] <[Deleted User]> #11
an...@google.com <an...@google.com> #12
You probably want to try to use `.cachedIn(viewModelScope)`. See
[Deleted User] <[Deleted User]> #13
I mean that `.cachedIn(viewModelScope)` in the ViewModel solved my scroll state issue.
eu...@gmail.com <eu...@gmail.com> #14
In my case .cachedIn(viewModelScope)
preserves data from loading again, but scroll position is still resetting to the start every time I navigate to another composable and back. I use LazyColumn
with header (item
element before items
), so that might be the case
da...@gmail.com <da...@gmail.com> #15
Check if you have any other item in the LazyList outside the paged items it will lose position.
If you have any comment it out and see if it still lose position.
as...@gmail.com <as...@gmail.com> #16
We are using paging 3 for loading with LazyPagingItems.
var listState: LazyListState //declared in the viewmodel
sample:
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxWidth()
) {
items(items = lazyListingItems,)
{
ListingCard(), onClick = {
navHostController.navigate(Routes.detail)
}
)
}
}
Please help to solve this.
an...@google.com <an...@google.com> #17
I think what happens is when you go to other screen your state still exist together with the ViewModel, then when you go back to the original screen lazyListingItems are recreated, and as Paging provides the values via a Flow there is no way to get the data synchronously, so there is always at least the first frame where lazyListingItems has 0 items yet, then LazyColumn is populated with 0 items so it has to reset the scroll position. It can't differentiate between the events where there are no items anymore and there are no items yet.
But if the LazyListState will be recreated as part of the recomposition then it can postpone the scroll position restoration till the first frame when there are at least 1 element in the list
ba...@gmail.com <ba...@gmail.com> #18
I have found a workaround that works for me.
The LazyPagingItems.kt
file was copied from the androidx.paging:paging-compose
artifact into my project (it doesn't have any other dependencies).
I modified it to add a flag inside the LazyPagingItems
class:
/**
* The number of items which can be accessed.
*/
val itemCount: Int get() = itemSnapshotList.size
// The code below was added:
/**
* Added to fix scroll state restoration - wait for this flag to be set to true
* before setting your [LazyListState] on the [LazyColumn].
*
* @author Baptiste Candellier
*/
var hasRestoredItems: Boolean = false
private set
The flag is set inside the updateItemSnapshotList()
method
private fun updateItemSnapshotList() {
itemSnapshotList = pagingDataDiffer.snapshot()
// The code below was added:
hasRestoredItems = true
}
Then on my composable, I wait for this flag to become true to pass the "real" LazyListState
, that comes from a ViewModel
.
LazyColumn(
// Wait for the items to be restored to set the real list state, so that it isn't reset in the meantime
state = if (lazyPagingItems.hasRestoredItems) lazyListState else LazyListState()
) {
// ... your items here
There might be edge cases but it works for me, I hope it helps.
im...@gmail.com <im...@gmail.com> #19
// Save and restore list state
fun lazyListSaver(): Saver<MutableState<LazyListState>, *> = listSaver(
save = { listOf(it.value.firstVisibleItemIndex, it.value.firstVisibleItemScrollOffset) },
restore = {
mutableStateOf(LazyListState(
firstVisibleItemIndex = it[0],
firstVisibleItemScrollOffset = it[1]
))
}
)
// Usage
fun ProfileTab(lazyListState: MutableState<LazyListState>)
LazyVerticalGrid(
state = lazyListState.value,
)
}
Hope this solution works.
an...@google.com <an...@google.com> #20
rememberLazyListState() is doing pretty much the same internally. Could you please elaborate a bit more on why the build in solution doesn't work for you?
im...@gmail.com <im...@gmail.com> #21
Ouch! I'm new on Compose, so don't know how state, rememberLazyListState() works so i directly pass
fun ProfileTab() LazyVerticalGrid( state = rememberLazyListState(), ) }
and while i switch tab from 1->2 and back from 2-> the state of LazyVerticalGrid regenerated, but i didn't try to save in state on globally and pass in composable.
After i seeing this
wh...@gmail.com <wh...@gmail.com> #22
as a lot of people mentioned, LazyColumn can't restore position correct when Paging has a Header
this is because rememberLazyListState can restore position when there are at least 1 element in the list
but if there is a Header or Footer in LazyColumn, then it will restore in first frame when paging's item count is still 0
so I store listState in ViewModel and restore it when Paging item count > 0
val listState = if (pagingData.itemCount > 0) viewModel.listState else rememberLazyListState()
ro...@gmail.com <ro...@gmail.com> #23
Try this workaround, you need to check lazyPagingItems before use:
@Composable
fun MainFrame(lazyPagingItems: LazyPagingItems<PerformanceEvent.Building>) {
val refresh = lazyPagingItems.loadState.refresh
// My temp solution
if (lazyPagingItems.itemCount == 0 && refresh is LoadState.NotLoading ) return //skip dummy state, waiting next compose
LazyColumn { ... }
}
ne...@gmail.com <ne...@gmail.com> #24
Here is a workaround:
@Composable
fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
// After recreation, LazyPagingItems first return 0 items, then the cached items.
// This behavior/issue is resetting the LazyListState scroll position.
// Below is a workaround. More info: https://issuetracker.google.com/issues/177245496.
return when (itemCount) {
// Return a different LazyListState instance.
0 -> remember(this) { LazyListState(0, 0) }
// Return rememberLazyListState (normal case).
else -> androidx.compose.foundation.lazy.rememberLazyListState()
}
}
Should we still expect a fix from any Jetpack library ?
ke...@gmail.com <ke...@gmail.com> #25
Could you provide a version name that contains the fix?
su...@gmail.com <su...@gmail.com> #26
Only thing that works is disabling phone rotation, or creating the pager flow before the NavHost. Are there any solutions?
wh...@gmail.com <wh...@gmail.com> #27
If your project does not have a deep dependency on the paging3 library, I recommend implementing a simple paging library yourself to avoid all the weird features of paging3. Paging library needs the coordination of the view layer and the model layer. It doesn't make sense to encapsulate these together.
Here's an example from my project. If you don't need to modify the list, it can be implemented much simpler.
da...@gmail.com <da...@gmail.com> #28
al...@mercari.com <al...@mercari.com> #29
Still not fixed in latest versions, workaround works. Please consider re-opening the issue.
co...@gmail.com <co...@gmail.com> #30
jo...@kalderstam.se <jo...@kalderstam.se> #31
Seconded. Still a problem in 1.3.1
[Deleted User] <[Deleted User]> #32
In our case, it seems to be triggered by adding headers/footers to the `LazyColumn`.
Please consider re-opening the issue.
il...@google.com <il...@google.com> #33
The default case (all of your items are loaded asynchronously) was fixed in cachedIn
in your layer above Compose (e.g., your ViewModel layer).
However, the case where you have some items available immediately (like a header and footer) and some items available only asynchronously, is not fixed. This is broken whether you use Paging or not - any data loaded from a Flow
would suffer this same problem (as they also would not be available in the first composition).
Creating an generic API for informing the Lazy Layout of when all asynchronously loaded items have finished loading (particularly noting that there can be multiple items
blocks loading from multiple asynchronous sources) is not something we can solve in the short or medium term.
That being said, we believe there may be a way to solve this in the specific case of headers+footers and a single call to collectAsLazyPagingItems
without having to wait for an overhaul of either the Paging or Lazy layout APIs, so we will be reopening this issue to track that internal investigation.
bi...@gmail.com <bi...@gmail.com> #34
pa...@outlook.com <pa...@outlook.com> #35
Sounds like Paging and Component are being developed with a lot of various serious issues that are piling with each version, and the team does not have any intention of fixing them. That's alright, at least now we can finally stop waiting for this to be fixed and instead stop using them entirely. Good luck with this.
cl...@google.com <cl...@google.com> #36
Compose requires an initial state for valid first composition.
Paging 3's entire architecture is based around emitting data through a Flow that doesn't emit until collected.
Therefore, significant changes within Paging 3 are required to enable initial states in paging-compose without collection. While these changes are not easily resolvable, we hope to address them in the coming year.
au...@gmail.com <au...@gmail.com> #37
vi...@gmail.com <vi...@gmail.com> #39
ty...@gmail.com <ty...@gmail.com> #40
ap...@google.com <ap...@google.com> #41
Branch: androidx-main
commit 4a567a83c33a3f15556d4777e97327e418cbe42e
Author: Clara Fok <clarafok@google.com>
Date: Wed Apr 26 12:29:43 2023
Initialize PagingDataDiffer with cached data
PagingDataDiffer constructor now accepts a nullable PagingData containing a nullable cached PageEvent.Insert. PagePresenter and combinedLoadStatesCollection will be initialized wiht the cached data if present. This allows cached items and loadStates to be accessible immediately without requiring any collection.
Note that HintReciver and UiReceiver is not set, hence differ will not respond to any UI input (scrolling / refresh etc) until a real PagingData has been collected from.
Test: ./gradlew paging:paging-common:test
Bug: 177245496
Change-Id: I3a9a80486a1e345699745311dcb585efbf7939bd
M paging/paging-common/src/main/kotlin/androidx/paging/PagePresenter.kt
M paging/paging-common/src/main/kotlin/androidx/paging/PagingDataDiffer.kt
M paging/paging-common/src/test/kotlin/androidx/paging/PagingDataDifferTest.kt
ap...@google.com <ap...@google.com> #42
Branch: androidx-main
commit ac2a48843976a46cdf9eb6db1d4d08d59deecbd8
Author: Clara Fok <clarafok@google.com>
Date: Tue Apr 25 16:43:38 2023
Exposed cached data via PagingData
When using Flow<PagingData<T>>.cachedIn(scope), data that has been collected within that generation is cached for as long as the scope remains alive.
Normally the cachedData can only be accessed by new subscriptions when they collect on the SharedFlow returned by cachedIn. Now we expose this cachedData via PagingData so that the cached data can be accessed synchronously without requiring collection.
Test: ./gradlew paging:paging-common:test
Bug: 177245496
Change-Id: I36a894d7d90f61396604e7847c5be6027b5e0991
M paging/paging-common/src/main/kotlin/androidx/paging/CachedPageEventFlow.kt
M paging/paging-common/src/main/kotlin/androidx/paging/CachedPagingData.kt
M paging/paging-common/src/main/kotlin/androidx/paging/PagingData.kt
M paging/paging-common/src/test/kotlin/androidx/paging/CachingTest.kt
8h...@gmail.com <8h...@gmail.com> #43
ap...@google.com <ap...@google.com> #44
Branch: androidx-main
commit 75b22ef9f04fc4d615b75f65d904dd4cc765bffd
Author: Clara Fok <clarafok@google.com>
Date: Thu Apr 27 14:03:07 2023
Cached data immediately available in LazyPagingItems
Paged data can be cached via pager.flow.cachedIn(scope). If collecting LazyPagingItems from this cachedIn flow, any cached data will be made available for presentation upon #collectAsLazyPagingItems initialization without requiring asynchronous collection.
This means that upon configuration change or navigating away and back, instead of LazyPagingItems containing zero items, it will now contain previously loaded data that can be immediately presented within initial composition. This helps preserve scroll state and a smoother UI transition.
Note that this only applies to flows returned directly from pager.flow.cachedIn(scope), so mapping or transformations should be applied prior to cachedIn.
Test: ./gradlew paging:paging-compose:cC
Fixes: 177245496
Relnote: "Cached data from pager.flow.cachedIn that has been collected in LazyPagingItems will now be immediately available after state restoration without requiring asyncrhonous collection. This means the cached data will be ready for presentation immediately upon initial composition after state is restored."
Change-Id: I97a6078c0563f8017af24448c32e59b86a987465
M paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
M paging/paging-compose/src/main/java/androidx/paging/compose/LazyPagingItems.kt
cl...@google.com <cl...@google.com> #45
This has been fixed internally and will be available in the next release. The next release version is TBD.
In short, the fix works by making cached data immediately available for presentation after config change / navigation. This prevents the list from having zero items right after config change/ navigation so the scroll state will be preserved without requiring workarounds such as
That said, it only works if there is cached data to begin with. This means you need pager.flow.cachedIn(scope)
. Note that any PagedData mapping/transformations should be applied prior to cachedIn(), such as
val pager = pager.flow.map { pagingData ->
pagingData.map {
// map items
}
}.cachedIn(scope)
8h...@gmail.com <8h...@gmail.com> #46
Is it going to fix incorrect loadState
(Loading
) as well (to avoid displaying incorrect refresh indicator to users)?
val lazyPagingItems = viewModel.pagingDataExample.collectAsLazyPagingItems()
val loadState = lazyPagingItems.loadState.refresh // loadState == Loading initially on config changes/navigation
It should be NotLoading
if data is returned from cahce and no request to refresh is triggered
cl...@google.com <cl...@google.com> #47
Yes it will start with NotLoading
if displaying cachedData.
na...@google.com <na...@google.com> #48
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.paging:paging-common:3.2.0-alpha05
il...@google.com <il...@google.com> #49
I'll note that while some of the changes from this bug made it into
ju...@google.com <ju...@google.com> #50
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.paging:paging-compose:1.0.0-alpha20
so...@gmail.com <so...@gmail.com> #51
cl...@google.com <cl...@google.com> #52
paging-common 3.2.0-alpha06
?
il...@google.com <il...@google.com> #53
Re
so...@gmail.com <so...@gmail.com> #54
is...@gmail.com <is...@gmail.com> #55
Regarding the workaround from firstVisibleItemIndex
and firstVisibleItemScrollOffset
to continue to remain the same in case other code depends on them, it looks like the workaround should be modified to this?
@Composable
fun <T : Any> LazyPagingItems<T>.rememberLazyGridState(): LazyGridState {
val state = androidx.compose.foundation.lazy.grid.rememberLazyGridState()
return when (itemCount) {
// Return a different LazyListState instance.
0 -> remember(this) {
LazyGridState(
firstVisibleItemIndex = state.firstVisibleItemIndex,
firstVisibleItemScrollOffset = state.firstVisibleItemScrollOffset
)
}
// Return rememberLazyListState (normal case).
else -> state
}
}
h9...@gmail.com <h9...@gmail.com> #56
If there is a combined flow, cachedIn does not seem to work. Could you please confirm?
val pager = pager.flow
.map { pagingData ->
pagingData.map {
// map items
}
}
.cachedIn(scope)
.combine(otherFlow) { pagingData, otherFlow ->
pagingData.map{
// something
}
}
.cachedIn(scope)
If you call the pager again with collectAsLazyPagingItems()
, LoadState will be called LoadState.Loading
rather than LoadState.NotLoading
.
cl...@google.com <cl...@google.com> #57
Re
jo...@mozilla.com <jo...@mozilla.com> #58
The workarounds, like in
il...@google.com <il...@google.com> #59
Re
Please file a new issue with a sample project that reproduces your issue if you are seeing something different.
pa...@gmail.com <pa...@gmail.com> #60
im having a similar problem as
cl...@google.com <cl...@google.com> #61
Re
pr...@gmail.com <pr...@gmail.com> #62
@Composable
fun <T : Any> LazyPagingItems<T>.rememberLazyListState(): LazyListState {
val state = androidx.compose.foundation.lazy.rememberLazyListState()
val scrollIndex = rememberSaveable { mutableIntStateOf(0) }
val coroutineScope = rememberCoroutineScope()
// After recreation, LazyPagingItems first return 0 items, then the cached items.
// This behavior/issue is resetting the LazyListState scroll position.
return when (itemCount) {
// Return a different LazyListState instance.
0 -> rememberSaveable(saver = LazyListState.Saver) {
scrollIndex.intValue = state.firstVisibleItemIndex
LazyListState(state.firstVisibleItemIndex, state.firstVisibleItemScrollOffset)
}
else -> {
if(scrollIndex.intValue in 1..<itemCount) {
// Return state after scrolling.
LaunchedEffect("") {
coroutineScope.launch {
state.animateScrollToItem(scrollIndex.intValue, state.firstVisibleItemScrollOffset)
scrollIndex.intValue = 0
}
}
}
state
}
}
}
se...@gmail.com <se...@gmail.com> #63
I want to share my case with a positive result.
I use compose paging 3 (api + room), navigation.
This is my case in detail.
1. Screen A (list with api + room pagination)
2. Screen B (details + changing data in the database by item ID)
When I changed the data on screen B and returned to screen A, the position was reset.
The reason was as follows. When my LazyPagerItems<*> object was with ItemCount == 0, I used item {} to show placeholder. That was the reason! When I started using items even with ItemCount == 0, the position reset stopped happening.
Bad Case:
Lazy Column() {
when {
values == null || values.itemCount == 0 -> item {} //simple item
items () {} //for paging items
}
}
Nice Case:
Lazy Column() {
when {
values == null -> item {} //simple item
items (
count = values.itemCount,
) {} //for paging items
}
}
Then I started adding elements to LazyColumn and making it combined. In this case, it also reset the position! By removing the item at the beginning, the list began to work correctly.
Bad Case:
Lazy Column() {
item {} //simple item
item {} //simple item
items () {} //for paging items
}
Nice Case:
Lazy Column() {
items () {} //for paging items
}
to...@gmail.com <to...@gmail.com> #64
Yes they do not want the if and placeholders to be inside the LazyColumn we are supposed to use if outside of the LazyColum.
The workaround is to use a fake lazystate until your data is loaded.
See for example solution
cl...@google.com <cl...@google.com> #65
re cachedIn()
, you shouldn't need a workaround. Can you upload a sample app for me to take a look?
to...@gmail.com <to...@gmail.com> #66
Cachein does not keep the data when the app is killed in the background or viewmodel is unloaded.
sa...@gmail.com <sa...@gmail.com> #67
to...@gmail.com <to...@gmail.com> #68
That's cute but you might actually want to read the issue.
hu...@nevix.tech <hu...@nevix.tech> #69
val pager = pager.flow
.map { pagingData ->
pagingData.map {
// map items
}
}
.cachedIn(scope)
.combine(otherFlow) { pagingData, otherFlow ->
pagingData.map{
// something
}
}
.cachedIn(scope)
my-version = androidx.paging:paging-compose:3.3.5
When otherFlow updates data, an error occurs: Attempt to collect twice from pageEventFlow, which is an illegal operation. Did you forget to call Flow<PagingData<*>>.cachedIn(coroutineScope)?
Description
Jetpack Compose release version: 1.0.0-alpha09 Android Studio Build: Build #AI-202.7319.50.2031.7049475, built on December 24, 2020
Steps to Reproduce:
NavHost
-ed composable screen containing aLazyColumn
built withLazyPagingItems
collected from a view modelResult: the LazyColumn is scrolled back to top
Expected: the scroll position should be remembered
Sample code that works:https://gist.github.com/CyrilFind/5c7d108dd8d030fe7a6a556e43f29f6f
If the
.collectAsLazyPagingItems()
line from this sample is done before declaring theNavHost
, it works as expected but if it is done inside thecomposable
block, it does not.