Some composables (swipeable, BottomSheet*, Horizon...
# compose
d
Some composables (swipeable, BottomSheet*, HorizontalPager, LazyColumn etc) have some internal state which they manage by themselves and even when this state is allowed to be hoisted the need for detecting some events based on state changes arises. I find myself wishing to write
if (prevState = 1 && state = 0) onEvent(...)
. Sometimes this leads me to discovery that I think about the state wrong and I refactor, but sometimes I can't and I ask myself what's the best pattern to detect some thresholds in state changes? Examples: • ListColumn has just scrolled past 100 visible item, how to report this to analytics? • Composable with
swipeable
modifier has just finished settling after swipe. How to observe this change from
swipeableState.isAnimationRunning
from
true
to
false
and report it to some other component?
for swipeable example I tried:
Copy code
log("is animating ${swipeableState.isAnimationRunning}")
val flow = snapshotFlow { swipeableState.isAnimationRunning }
LaunchedEffect(Unit) {
  flow.collect {
    log("animated changed to ${it}")
  }
}
this logs a lot of "is animating" which changes from
false
to
true
to
false
, but "animated changed" is logged only once as start as
false
. So I guess snapshotFlow is not for this usecase or maybe it's even an antipattern here. What's the right way to go then?
z
Two things that I don't think are related to this issue, but are anti patterns nonetheless: 1. You should create the snapshot flow inside the launched effect if that's the only place you're using it from. Otherwise you're creating a new flow on every recomposition but only using the first one. The rest are just discarded, so creating them is pointless. 2. Always pass dependencies to your effect as keys. In this case, your effect has a dependency on swipeableState, so it should be passed. You almost never want to use
Unit
.
🙏 1
d
oh, both valid points. I have tried the first, but failed at the second. It works now! So answering a more generic question: using
snapshotFlow
for those cases is a valid way to go?
z
That is surprising - it means you're actually getting a different swipeableState object whenever the animation changes, but the state should live longer than that. How are you creating your swipeable state?
But yea to detect state changes like this you could use flow operators, something like:
Copy code
snapshotFlow { state }
  .distinctUntilChanged()
  .drop(1)
  .filter { it == 0 }
Or
Copy code
snapshotFlow { state }
  .scan { prev, new -> 
    prev == 1 && new == 0
  }
  .distinctUntilChanged()
🎉 1
d
Copy code
val swipeableState = rememberSwipeableState(initialValue = findInitialAnchorId(state).value)
LaunchedEffect(swipeableState) {
  val flow = snapshotFlow { swipeableState.isAnimationRunning }
  flow.collect {
    Timber.e("animated changed to ${it}")
  }
}
This works. I have logged "swipeableState" and it doesn't change object-hex pointer, so it's the same)