I want to know whether the last item of a lazy lis...
# compose
s
I want to know whether the last item of a lazy list is available and I am having trouble making that happen without making my composable recompose endlessly. And by endlessly I mean
SideEffect {}
triggers more than 10 times a second forever without anything changing on the screen. More details in thread đŸ§”
So in order to test this, I am simply logging the recomposes by doing
SideEffect { Timber.d("Recompose Triggered") }
as it says that it runs after every recomposition therefore I hope this is the correct approach. When I don’t add any of the code I will post below this, the endless recomposition does not happen so I am fairly certain this is what brings the problem. I want to have a function that has the signature of
Copy code
fun LazyListState.isLastItemVisibleAsState(): State<Boolean>
For the implementation, I have tried numerous things so far, with all of them triggering the infinite recomposition or having other problems. To start with, I tried this
Copy code
val lastItemIndex = remember { mutableStateOf(this.layoutInfo.visibleItemsInfo.lastOrNull()?.index) }
return remember(this) {
    derivedStateOf {
        lastItemIndex.value == this.layoutInfo.totalItemsCount - 1
    }
}
But this one obviously doesn’t work as lastItemIndex simply captures the value on first composition and then never updates So I thought I’d provide as the key to remember the index itself thinking that when it changes, the remember should re-calculate the value it holds like this:
Copy code
val lastItemIndex = remember(layoutInfo.visibleItemsInfo.lastOrNull()?.index) {
    mutableStateOf(layoutInfo.visibleItemsInfo.lastOrNull()?.index)
}
return derivedStateOf {
    lastItemIndex.value == this.layoutInfo.totalItemsCount - 1
}
But this triggers recomposition forever. I assumed that what would happen here is the
layoutInfo.visibleItemsInfo.lastOrNull()?.index
returns the same index, therefore the mutable state does not change, therefore there is no need for a recomposition. I thought maybe I should wrap the
derivedStateOf
with a
remember
call but this didn’t help either. I at that point noticed that layoutInfo.visibleItemsInfo returns a different set of items on every recomposition, and it’s just a list not coming as
State<T>
, so maybe this has something to do with all of this? I don’t know how to approach this problem, which mechanisms should be used (
remember
derivedStateOf
etc.) and how to understand why what I am doing is not what Compose expects me to do and does all these unnecessary recompositions. So my question would be, how would you implement this function with the signature that I propose at the beginning, and if you do have a nicely working solution, why does that work while my approaches have all been unsuccessful? Also, am I just tripping at this point, am I doing something incredibly and fundamentally wrong? I do not know.
d
fun LazyListState.isLastItemVisibleAsState(): State<Boolean>
is not a good function imo. I think you definitely just want,
fun LazyListState.isLastItemVisibleAsState(): Boolean
.
That should make things much easier for ya. Don't use
remember
or
derivedStateOf
for "performance" until you measure/benchmark.
â˜đŸ» 1
s
Hmm interesting, I do want it to update whenever the contents of
LazyListState
change, so I thought it would make sense to return it as a
State
to denote that the result of this will be forcing recomposition (when it legitimately changes, not all the time). Is this a wrong approach, how would I denote that with a function that seemingly just returns a boolean as a one shot operation?
d
If you call
isLastItemVisibleAsState
in a composable function, it'll subscribe to what ever state it touches and update itself accordingly.
I guess what I'm saying is,
LazyListState
is already a
State<...>
so all you need to do is read from it.
s
Okay, but still, with
Copy code
val isLastItemVisible = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == lazyListState.layoutInfo.totalItemsCount - 1
on my composable, it recmposes infinitely. Shouldn’t that “just work” if it is a State?
d
Fascinating, you can wrap that with
rememberUpdatableState()
then.
val isLastItemVisible by rememberUpdatableState(lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == lazyListState.layoutInfo.totalItemsCount - 1)
s
Nope that doesn’t seem to work either. specifically
visibleItemsInfo
I think is the one that returns a different list each time and that maybe is what is messing this all up. It feels like everything I t*hink* I know about compose goes out of the window with this problem.
d
How does it not work?
s
I mean I am still getting SideEffect {} to trigger multiple times a second without touching the screen or having anything change on the screen. And if I remove this line, this doesn’t happen anymore. And I am not even using the result
isLastItemVisible
it’s there greyed out as not-used. And just to clarify, by
rememberUpdatableState()
I assume you mean
rememberUpdatedState()
right?
d
Yeah sure, I don't have docs or the IDE open rn.
Ah I see, I guess you could use deriveStateOf in that case to defer the read.
val isLastItemVisible by remember { derivedStateOf { lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == lazyListState.layoutInfo.totalItemsCount - 1 } }
s
Okay, this seems to not be triggering infinite recompositions đŸ„ł Now I am almost mad that I don’t understand why I could not recreate this with the approaches I have done before. Like this makes sense, but this is so close to what I was doing before too, only that we are no longer remembering something outside of the
derivedStateOf
and are simply putting everything in there for it to handle for us automatically. Ah sometimes it’s somewhat frustrating, like what do I need to read to be able to understand all of this? Just trial and error until it “clicks”?
d
Errm, I've learned most of what I know from stalking this channel and answering people's questions. Typically after an answer Adam Powell comes in and gives me a đŸ‘đŸŒ, adds more detail or corrects me.
s
Adam Powell driven development. My favourite 😅
😂 7
d
I'd recommend using Slack search to read all his answers that have
State
in them 😄 .
➕ 1
🙌 1
Hahahaha
s
Time to put “Adam Powell driven development” on my CV now, if the employer recognises his name, you know it’s the right place to work 😂
😄 6
fun LazyListState.isLastItemVisibleAsState(): State<Boolean>
 is not a good function imo. I think you definitely just want,
fun LazyListState.isLastItemVisibleAsState(): Boolean
I just had another discussion around this with some different context about another function with the same signature difference and I feel like you would be interested in reading it especially after we had this discussion here before @Dominaezzz It seems like not only is this function signature not necessarily “not a good function” but it also come with some potential benefits as discussed here https://kotlinlang.slack.com/archives/CJLTWPH7S/p1629839151061400 . And after I changed by implementation it also limits the recompositions only to the scope that actually use this value. Just thought of sharing it with you because I am sure you would be interested in it 😄
đŸ‘đŸŒ 1
d
Ah, the story is different for composable functions vs non-composable functions. Especially since you can't put the former in remember or derived state.
s
But in both our examples we were dealing with composable functions
d
Oh wow, this whole time I thought isLastItemVisible was non-composable. Ignore everything said 😂
s
Lmao I'm dead â˜ ïžđŸ˜‚