Hey all, i have this modifier function that record...
# compose
m
Hey all, i have this modifier function that records when a given item index in a LazyColumn or LazyRow comes into the viewport. Internally, it's a "composed" style modifier.
Copy code
fun Modifier.onItemImpression(
    state: LazyListState,
    key: Any = true,
    onImpression: (Int) -> Unit
): Modifier
I'm writing an extension to it only do the given callback a single time for each encountered value. My question is should the extended modifier by "composed" or have an @Composable annotation on the function itself? Is there an advantage to one over the other?
Copy code
@Composable
fun <T> onlyOnce(callback: (T) -> Unit): (T) -> Unit { ... }

@Composable
fun Modifier.onItemFirstImpression(
    state: LazyListState,
    key: Any = true,
    onImpression: (Int) -> Unit
): Modifier = onItemImpression(
    state = state,
    key = key,
    onImpression = onlyOnce { onImpression(it) }
)

fun Modifier.onItemFirstImpression(
    state: LazyListState,
    key: Any = true,
    onImpression: (Int) -> Unit
): Modifier = composed {
    onItemImpression(
        state = state,
        key = key,
        onImpression = onlyOnce {
            onImpression(it)
        }
    )
}
l
composed
is discouraged for performance reasons,
@Composable
modifier factory is preferred if you need to use something from composition. It’s not clear from the snippet, but you might not even need a
@Composable
factory here if the logic inside
onlyOnce
can be implemented inside a Modifier.Node implementation
m
As a side note, while i'm trying to test this, i've noticed that the VisibleItems never has a size other than 0. Is there something about LazyList inside a "runComposeUITest" block that prevents ti from having visible items? I can see the semantic tree itself has items, but my modifier is based on the values inside of that viisible items.
Also i haven't worked with custom modifier nodes before, so it's unclear to me which type i would need to implement. My onlyOnce callback is simply remembering the parameters that the function is called with and ensuring that it's never calling it twice with the same value
l
Does this thing need to be a modifier? Or can it be a Composable function
m
Here's the original modifier:
Copy code
fun Modifier.onItemImpression(
    state: LazyListState,
    key: Any = true,
    onImpression: (Int) -> Unit
): Modifier {
    return composed {
        val flow = snapshotFlow { state.layoutInfo }
        val visibleItemIndexes = remember { mutableStateOf(emptyList<Int>()) }

        LaunchedEffect(key1 = key) {
            flow.collect { layoutInfo ->
                val newVisibleItemIndexes = layoutInfo.visibleItemsInfo.filter { item ->
                    item.offset >= layoutInfo.viewportStartOffset && item.size + item.offset <= layoutInfo.viewportEndOffset
                }.map { item -> item.index }

                val newlyVisible = newVisibleItemIndexes - visibleItemIndexes.value

                newlyVisible.forEach {
                    onImpression(it)
                }

                visibleItemIndexes.value = newVisibleItemIndexes
            }
        }

        Modifier
    }
}
The intent here is to fire a callback whenever an item in a LazyListState becomes newly available in the viewport. So if you are viewing indexes 1 -20, when you scroll and items 11-30 are now visible you'd get callbacks for items 21-30.
l
This looks like it can just be a Composable function, you're not even returning a real modifier here?
m
I wrote this a long time ago may compose versions ago, so it's probably not the most efficient way to do things, I know.
At the time, a Modifier seemed the right thing to do. I'm open to other ideas. But i'm also struggling to test this because the layout info isn't working right in the test environment
l
Not sure what the test environment issue is, maybe a synchronization thing
But you should be able to extract everything inside the composed {} and put this in a
@Composable fun OnItemImpression(...)
m
But i also have an
onImpression
modifier that's similar, but meant for things that are in a standard Row or Column and uses LocalView.current inside the composed block
And it's not sychronization i don't think in terms of the test env. I have a forcible 5s delay in there just to make sure, but layoutInfo never gets updated
l
One issue here is that snapshotFlow isn't remembered, you can just move the snapshotFlow inside the launched effect body. Not sure if this is the only issue
m
I don't think that's the issue. I did try moving the snapshotFlow call into the LauchedEffect, but the collect is only ever getting called once, so it seems like the LayoutInfo isn't getting updated.