Status Update
Comments
an...@google.com <an...@google.com>
b9...@gmail.com <b9...@gmail.com> #3
When updating the providers, startProvider()
is comparing the new scope produces with the previous scope produced, instead of the current parent scope, to determine if the content of the provider needs to change, ignoring if the parent changed. That has the effect of, if a provider provides a value that is identical to the parent value, the composer thinks that none of the the static composition locals changed and it doesn't need to force updates of the content of the provider.
The temporary work-around for this is to use a compositionLocalOf
instead. Using compositionLocalOf
is recommended for composition locals that can change and static should are not recommended for values that can change.
However, in the above example, LocalOtherValue
only changes once so using a staticCompositionLocal
is recommended as it avoids the overhead for tracking reads of a value that rarely changes, making this work-around temporary for LocalOtherValue
or similar locals.
an...@google.com <an...@google.com> #4
b9...@gmail.com <b9...@gmail.com> #5
This issue also affects Material theming changes because they use staticCompositionLocalOf
under the hood. And those cannot be changed by a work-around.
an...@google.com <an...@google.com> #6
Project: platform/frameworks/support
Branch: androidx-main
Author: Chuck Jazdzewski <
Link:
Fixed providesDefault
for a single provide
Expand for full commit details
Fixed `providesDefault` for a single provide
If `providesDefault` is used as the sole provided value
of a `CompostionLocalProvider` then the content of the
provider may not update if a parent changes the value
of a static composition local. Providing more than
one value does not have this issue.
Fixes: 374263387
Test: new test, ./gradlew :compose:r:r:test
Change-Id: Iabd4fbba6191abad4073ab5f302d252750683258
Files:
- M
compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
- M
compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
Hash: b70a4268ba4985d4a5baf9c14edd81290d2ded1d
Date: Thu Oct 24 17:16:39 2024
b9...@gmail.com <b9...@gmail.com> #7
The following release(s) address this bug.It is possible this bug has only been partially addressed:
androidx.compose.runtime:runtime:1.8.0-alpha06
androidx.compose.runtime:runtime-android:1.8.0-alpha06
androidx.compose.runtime:runtime-jvmstubs:1.8.0-alpha06
androidx.compose.runtime:runtime-linuxx64stubs:1.8.0-alpha06
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
ca...@sliide.com <ca...@sliide.com> #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.