Can LaunchedEffects dsl's triggered twice when the...
# compose
o
Can LaunchedEffects dsl's triggered twice when the property of the key has not been changed? In our case the LE below is getting triggered twice in case of value of isInViewport is
true
. The composition is in viewport when rendered which is why the value is
true
in first place and the initial value of it is
false
. Expected behavior is that it only gets triggered once right away when its displayed and not twice for the same value
true
Copy code
var isInViewport by remember { mutableStateOf(false) }

    Box(modifier.onViewport { inViewport, componentPosition ->
                    isInViewport = inViewport
                    currentComponentPosition = componentPosition
                })
    
    LaunchedEffect(isInViewport) {
        if (isInViewport) {
            uim.onDisplay?.invoke(currentComponentPosition)
        } else {
            uim.onOutOfViewport?.invoke(null)
        }
    }
p
If inside a LazyColumn is possible
o
how come? and how can we fix it @Pablichjenkov
p
Save an article talking about that time ago, trying to find it.
🙏 1
f
When row of LazyColumn is no longer visible it will dispose of state for that row. rememberSaveable { } might work in this case
Copy code
var lastIsInViewport by rememberSaveable { mutableStateOf(!isInViewport) }

LaunchedEffect(isInViewport) { 
   if(lastIsInViewport != isInViewport) {
       lastIsInViewport =   isInViewport

       // code
   }
}
o
the above example is just a snippet for folks to have a quick look at the code
p
No luck finding it. I recall it was in a Twitter thread and somebody posted the link to some article. But not sure where I saved that link. But it is related to what Fabricio mentioned above. You can get scenarios where multiple attach/detached to/from composer due to ViewPort visibility. And also you can get a LaunchEffect trigger even before the composable being visible due to some subcomposition prefetch. You can check this s.o question: https://stackoverflow.com/questions/69884375/execute-launchedeffect-only-when-an-item-is-visible-in-lazycolumn
o
yeah i stumbled upon the same with google search earlier. thanks @Pablichjenkov
👍 1
😁 1
but still my use case might not work with just the visible item but infact needed a LE being triggered 😞
z
When is onViewport called? If it’s in the layout phase, then LaunchedEffect will be composed and started with the initial false value before getting the updated value on the next frame
1
o
onViewport
is our own custom modifier that looks like below which helps us to get the view's position in the window
Copy code
onGloballyPositioned { coordinates ->
        isInViewport = coordinates.isInViewport()

        val (x: Int, y: Int, width: Int, height: Int) = when {
            isInViewport -> with(coordinates.positionInRoot()) {
                listOf(x.toInt(), y.toInt(), coordinates.size.width, coordinates.size.height)
            }
            else -> listOf(0, 0, 0, 0)
        }
        componentPosition = ComponentPosition(
            topLeftX = x,
            topLeftY = y,
            width = width,
            height = height
        )
    }
z
right, so
onGloballyPositioned
happens even after the layout pass
1
o
so after having
Copy code
fun Modifier.onViewport(
    onDisplayedInViewport: (Boolean, ComponentPosition) -> Unit,
): Modifier = composed {
    var isInViewport by remember { mutableStateOf(false) }
    var isDisplayed by remember { mutableStateOf(false) }
    var componentPosition by remember { mutableStateOf(ComponentPosition.Default) }

    LaunchedEffect(isInViewport, isDisplayed, onDisplayedInViewport) {
        if (!isInViewport) {
            isDisplayed = false
            onDisplayedInViewport(isDisplayed, componentPosition)
        } else if (!isDisplayed) {
            isDisplayed = true
            onDisplayedInViewport(isDisplayed, componentPosition)
        }
    }

    onGloballyPositioned { coordinates ->
        isInViewport = coordinates.isInViewport()

        val (x: Int, y: Int, width: Int, height: Int) = when {
            isInViewport -> with(coordinates.positionInRoot()) {
                listOf(x.toInt(), y.toInt(), coordinates.size.width, coordinates.size.height)
            }
            else -> listOf(0, 0, 0, 0)
        }
        componentPosition = ComponentPosition(
            topLeftX = x,
            topLeftY = y,
            width = width,
            height = height
        )
    }
}
for a mofidier inside a column hosted by lazyColumn as such
Copy code
onViewport { inViewport, componentPosition ->
                    isInViewport = inViewport
                    currentComponentPosition = componentPosition
                }
which results in LaunchedEffect called twice in the below case
Copy code
var isInViewport by remember { mutableStateOf(false) }

Box(Modifier.
                onViewport { inViewport, componentPosition ->
                    isInViewport = inViewport
                    currentComponentPosition = componentPosition
                }
)

LaunchedEffect(isInViewport) {
        if (isInViewport) {
            uim.onDisplay?.invoke(currentComponentPosition)
        } else {
            uim.onOutOfViewport?.invoke(null)
        }
    }
but that should not have triggered the LaunchedEffect with key as boolean as value as true twice no?
z
LazyColumn doesn’t matter. You’re always going to have
isInViewport=false
on the first composition. Then you’ll do the first layout pass, get the
onGloballyPositioned
callback, which may set
isInViewport
to true, which will cause a recomposition on the next frame where you’ll see the new value. If the component is not in view immediately, then it shouldn’t recompose again until it comes into view.
But I wouldn’t use recomposition to handle this. You can do this instead:
Copy code
LaunchedEffect(uim) {
  snapshotFlow { isInViewport }.collect { isInViewport ->
    if (isInViewport) {
      …
    }
}
1
l
Why use
snapshotFlow
instead of a
LaunchedEffect
key?
z
To avoid recomposing and restarting an effect unnecessarily. It’s less work to just observe the state with a flow.
l
Interesting