Infinite recomposition on LazyColumn items in Jetpack Compose - android

I've got simple LazyColumn:
LazyColumn {
val lazySportEvents: LazyPagingItems<RecyclerItem> = stateValue.pagingItems.collectAsLazyPagingItems()
lazySportEvents.apply {
when (loadState.refresh) {
is LoadState.NotLoading -> {
itemsIndexed(
lazyPagingItems = lazySportEvents,
itemContent = { index, item ->
when (item) {
is SportEvent -> Text(item.name)
is AdItem -> AndroidView(
factory = { context ->
AdImageView(context).apply{
loadAdImage(item.id)
}
}
)
}
}
)
}
}
}
}
When I scroll screen down, everything loads fine. But when I scroll up, I end up with fun loadAdImage() called. It means that recomposition for AdItem happened even if that is the very same item (values and reference) like before scrolling screen down! Why does recomposition even happen then? I would like to omit it, to not load the same ad image every time while scrolling.
Is it even possible to skip recomposition for lazy paging items?
Edit: I realised the recomposition for items was infinite and that caused aforementioned behavior.

It turned out that when I changed
modifier = Modifier.fillParentMaxWidth()
to
modifier = Modifier.fillMaxWidth()
for some Boxes in the composable layout, infinite recomposition in LazyColumn stopped.
Edit: while working on the fix I experienced some Gradle dependencies caching problems. Anyway, when the problem approached for the 2nd time - the solution was to... upgrade compose.foundation dependency:
implementation "androidx.compose.foundation:foundation:$1.2.0-alpha07"
Not sure which one helped.

Related

LazyColumn recomposes the element multiple times on scroll. Performance issue

In the Log i can see if i just touch the list it will recompose it multiple times. It works fine and print "!!!" once every time new row appears if i run composable function separatly from app, but inside composeView it goes crazy and print it a lot of times.
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize().background(colorResource(R.color.dark_blue))
) {
itemsIndexed(items, { index, item -> index) }) { index, item ->
println("!!!")
TableRow(item)
}
}
the function itself only runs once

Inifinite loop loading items with paging3 and Jetpack compose UI

I have a simple app with a single screen, displaying movies in a Composable items list:
I use Android's paging3 library in order to load the movies page by page, and things seem to be working well:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
MoviesList(lazyMovieItems)
}
#Composable
fun MoviesList(lazyPagedMovies: LazyPagingItems<Movie>) {
LazyColumn(modifier = Modifier.padding(horizontal = 16.dp)) {
itemsIndexed(lazyPagedMovies) { index, movie ->
MoviesListItem(index, movie!!)
}
}
}
In an attempt to add a progress indicator to the initial loading phase (e.g. as explained in an Android code-lab), I've tried applying the following conditional, based on loadState.refresh:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
// Added: Show a progress indicator while the data is loading
if (lazyPagedMovies.loadState.refresh is LoadState.Loading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
MoviesList(lazyMovieItems)
}
Instead of displaying the progress indicator, this naive addition seem to be putting the paging loader into an infinite loop, where the first page gets fetched over and over indefinitely, without any items effectively being loaded (let alone displayed) into the list.
Side note: Just to rule out that this all has something to do with the condition itself, it appears that even adding as little as this log: Log.i("DBG", ""+lazyPagesMovies.loadState) with no conditions at all, introduces the undesired behavior.
I'm using Kotlin version 1.7.10 and the various Compose libraries in version 1.3.1.
Seems that with this simple code I might have somehow hit some Compose related edge-case. I've managed to work around things by introducing the progress-indicator conditional under a sub-function (composable) that accepts the paging items directly:
#Composable
fun FlixListScreen(viewModel: MoviesViewModel) {
val lazyMovieItems = viewModel.moviesPageFlow.collectAsLazyPagingItems()
MoviesScreen(lazyMovieItems) // was: MoviesList(lazyMovieItems)
}
// Newly added intermediate function
#Composable
fun MoviesScreen(lazyPagedMovies: LazyPagingItems<Movie>) {
MoviesList(lazyPagedMovies)
if (lazyPagedMovies.loadState.refresh is LoadState.Loading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
#Composable
fun MoviesList(lazyPagedMovies: LazyPagingItems<Movie>) {
// ... (unchanged)
}

Compose - DropDownMenu is causing unwanted recomposition

This is the composable hierarchy in my app:
HorizontalPager
↪LazyVerticalGrid
↪PostItem
↪AsyncImage
DropdownMenu
↪DropdownMenuItem
When swiping the HorizontalPager the AsyncImage inside my PostItem's are recomposing.
Removing the DropdownMenu fixes this and the PostItem is no longe recomposing and gives the wanted behavior.
The problem is that I have huge FPS drops when swiping through the HorizontalPager.
Why is DropdownMenu causing a recomposition when swiping the HorizontalPager?
var showMenu by remember { mutableStateOf(false) }
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }) {
DropdownMenuItem(onClick = {
}) {
Text(text = "Share")
}
}
Unfortunately, without seeing more code showing the rest of the structure, it's hard to say for sure what your problem is here.
A likely answer is that you haven't split up your Composable function enough. Something the docs hardly talk about is that the content portion of a lot of the built-in Composeables are inline functions which means that if the content recomposes the parent will as well. This is the most simple example of this I can give.
#Composable
fun foo() {
println("recompose function")
Box {
println("recompose box")
Column {
println("recompose column")
Row {
println("recompose row")
var testState by mutableStateOf("my text")
Button(
onClick = { testState = "new text" }
) {}
Text(testState)
}
}
}
}
output is:
recompose function
recompose box
recompose column
recompose row
Not only does this recompose the whole function it also recreates the testState causing it to never change values.
Again not 100% that this is your problem, but I would look into it. The solution would be to split my Row and row content into it's own Composable function.

Jetpack Compose: nested LazyColumn / LazyRow

I've read through similar topics but I couldn't find satisfactory result:
What is the equivalent of NestedScrollView + RecyclerView or Nested RecyclerView (Recycler inside another recycler) in Jetpack compose
Jetpack Compose: How to put a LazyVerticalGrid inside a scrollable Column?
Use lazyColum inside the column has an error in the Jetpack Compose
Nested LazyVerticalGrid with Jetpack Compose
My use-case is: to create a comments' list (hundreds of items) with possibility to show replies to each comment (hundreds of items for each item).
Currently it's not possible to do a nested LazyColumn inside another LazyColumn because Compose will throw an exception:
java.lang.IllegalStateException: Vertically scrollable component was
measured with an infinity maximum height constraints, which is
disallowed. One of the common reasons is nesting layouts like
LazyColumn and Column(Modifier.verticalScroll()). If you want to add a
header before the list of items please add a header as a separate
item() before the main items() inside the LazyColumn scope. There are
could be other reasons for this to happen: your ComposeView was added
into a LinearLayout with some weight, you applied
Modifier.wrapContentSize(unbounded = true) or wrote a custom layout.
Please try to remove the source of infinite constraints in the
hierarchy above the scrolling container.
The solutions provided by links above (and others that came to my mind) are:
Using fixed height for internal LazyColumn - I cannot use it as each item can have different heights (for example: single vs multiline comment).
Using normal Columns (not lazy) inside LazyColumn - performance-wise it's inferior to lazy ones, when using Android Studio's Profiler and list of 500 elements, normal Column would use 350MB of RAM in my app comparing to 220-240MB using lazy Composables. So it will not recycle properly.
Using FlowColumn from Accompanist - I don't see any performance difference between this one and normal Column so see above.
Flatten the list's data source (show both comments and replies as "main" comments and only make UI changes to distinguish between them) - this is what I was currently using but when I was adding more complexity to this feature it prevents some of new feature requests to be implemented.
Disable internal LazyColumn's scrolling using newly added in Compose 1.2.0 userScrollEnabled parameter - unfortunately it throws the same error and it's an intended behaviour (see here).
Using other ways to block scrolling (also to block it programatically) - same error.
Using other LazyColumn's .height() parameters like wrapContentHeight() or using IntrinsicSize.Min - same error.
Any other ideas how to solve this? Especially considering that's doable to nest lazy components in Apple's SwiftUI without constraining heights.
I had a similar use case and I have a solution with a single LazyColumn that works quite well and performant for me, the idea is to treat your data as a large LazyColumn with different types of elements.
Because comment replies are now separate list items you have to first flatten your data so that it's a large list or multiple lists.
Now for sub-comments you just add some padding in front but otherwise they appear as separate lazy items.
I also used a LazyVerticalGrid instead of LazyColumn because I've had to show a grid of gallery pictures at the end, you may not need that, but if you do, you have to use span option everywhere else as shown below.
You'll have something like this:
LazyVerticalGrid(
modifier = Modifier
.padding(6.dp),
columns = GridCells.Fixed(3)
) {
item(span = { GridItemSpan(3) }) {
ComposableTitle()
}
items(
items = flattenedCommentList,
key = { it.commentId },
span = { GridItemSpan(3) }) { comment ->
ShowCommentComposable(comment)
//here subcomments will have extra padding in front
}
item(span = { GridItemSpan(3) }) {
ComposableGalleryTitle()
}
items(items = imageGalleryList,
key = { it.imageId }) { image ->
ShowImageInsideGrid(image) //images in 3 column grid
}
}
I sloved this problem in this function
#Composable
fun NestedLazyList(
modifier: Modifier = Modifier,
outerState: LazyListState = rememberLazyListState(),
innerState: LazyListState = rememberLazyListState(),
outerContent: LazyListScope.() -> Unit,
innerContent: LazyListScope.() -> Unit,
) {
val scope = rememberCoroutineScope()
val innerFirstVisibleItemIndex by remember {
derivedStateOf {
innerState.firstVisibleItemIndex
}
}
SideEffect {
if (outerState.layoutInfo.visibleItemsInfo.size == 2 && innerState.layoutInfo.totalItemsCount == 0)
scope.launch { outerState.scrollToItem(outerState.layoutInfo.totalItemsCount) }
println("outer ${outerState.layoutInfo.visibleItemsInfo.map { it.index }}")
println("inner ${innerState.layoutInfo.visibleItemsInfo.map { it.index }}")
}
BoxWithConstraints(
modifier = modifier
.scrollable(
state = rememberScrollableState {
scope.launch {
val toDown = it <= 0
if (toDown) {
if (outerState.run { firstVisibleItemIndex == layoutInfo.totalItemsCount - 1 }) {
Log.i(TAG, "NestedLazyList: down inner")
innerState.scrollBy(-it)
} else {
Log.i(TAG, "NestedLazyList: down outer")
outerState.scrollBy(-it)
}
} else {
if (innerFirstVisibleItemIndex == 0 && innerState.firstVisibleItemScrollOffset == 0) {
Log.i(TAG, "NestedLazyList: up outer")
outerState.scrollBy(-it)
} else {
Log.i(TAG, "NestedLazyList: up inner")
innerState.scrollBy(-it)
}
}
}
it
},
Orientation.Vertical,
)
) {
LazyColumn(
userScrollEnabled = false,
state = outerState,
modifier = Modifier
.heightIn(maxHeight)
) {
outerContent()
item {
LazyColumn(
state = innerState,
userScrollEnabled = false,
modifier = Modifier
.height(maxHeight)
) {
innerContent()
}
}
}
}
}
All what I did is that:
At first I set the height of the inner lazyList to the height of the parent view using BoxWithConstraints, this lets the inner list fill the screen without distroying the lazy concept.
Then I controlled the scrolling by disable lazy scroll and make the parent scrollable to determine when the scroll affect the parent list and when the child should scroll.
bud this still has some bugs when the parent size changed , in my case I escaped by this SideEffect
You can use rememberScrollState() for root column. Like this;
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())) {
LazyColumn {
// your code
}
LazyRow {
// your code
}
}

Material Swipe To Dismiss in Compose maks incorrect items for dismissal

I'm implementing drag/swipe to dismiss functionality in a simple notepad app implemented in Compose. I've run into a strange issue where SwipeToDismiss() in a LazyColumn dismisses not only the selected item but those after it as well.
Am I doing something wrong or is this a bug with SwipeToDismiss()? (I'm aware that it's marked ExperimentalMaterialApi)
I've used the Google recommended approach from here:
https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#swipetodismiss
this is where it happens:
/* ...more code... */
LazyColumn {
items(items = results) { result ->
Card {
val dismissState = rememberDismissState()
//for some reason the dismmissState is EndToStart for all the
//items after the deleted item, even adding new items becomes impossible
if (dismissState.isDismissed(EndToStart)) {
val scope = rememberCoroutineScope()
scope.launch {
dismissed(result)
}
}
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
/* ...more code... */
and here is my project with the file in question
https://github.com/davida5/ComposeNotepad/blob/main/app/src/main/java/com/anotherday/day17/ui/NotesList.kt
You need to provide key for the LazyColumn's items.
By default, each item's state is keyed against the position of the
item in the list. However, this can cause issues if the data set
changes, since items which change position effectively lose any
remembered state.
Example
LazyColumn {
items(
items = stateList,
key = { _, listItem ->
listItem.hashCode()
},
) { item ->
// As it is ...
}
}
Reference

Categories

Resources