https://kotlinlang.org logo
a

AmrJyniat

08/26/2022, 9:45 AM
I noticed that
LaunchEffect()
has natural behavior like
distinctUntilChanged()
? I have an event for showing toast but it seems that show the toast only once. Code in 🧵
Copy code
// Event in VM
private val _event = Channel<LoginEvent>()
val event = _event.receiveAsFlow().shareIn(viewModelScope)
fun sendEvent(newEvent: LoginEvent) = viewModelScope.launch {
    _event.send(newEvent)
}

// Collect events in Composable
val uiEvent by viewModel.event.collectAsStateWithLifecycle(NoEvent)
LaunchedEffect(uiEvent) {
      when (uiEvent) {
          is FailedLoginEvent ->
              showToast(uiEvent.errorMsg) //showing toast only once even when sending a new event
                ....
      }
}
c

Csaba Szugyiczki

08/26/2022, 10:39 AM
If the key in your
LaunchedEffect
does not change, then it will not trigger until the given value changes. I guess your LoginEvent is an enum or object, for which every value is basically referring to the same instance, hence your LaunchedEffect only runs the first time you set your event value
make sure you create a new object every time you want to emit a new event
could you please show how your LoginEvent is defined?
s

Stylianos Gakis

08/26/2022, 10:40 AM
This has nothing to do with
LaunchedEffect
.
collectAsState
uses
produceState
under the hood which itself returns a
State
which by design is not built to re-emit the same result twice, that’s a way compose skips recomposing stuff when it doesn’t need to. So assuming that you want to keep your event in a Channel (maybe you don’t) You’d have to do something like this instead:
Copy code
val lifecycle = LocalLifecycleOwner.current.lifecycle
LaunchedEffect(viewModel.eventFlow) {
  lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
    viewModel.eventFlow.collect { event ->
      doStuffWith(event)
    }
  }
}
I realize Csaba replied as I was typing my message as well. Yes that’s also one way to look at it, but really taking a flow of events, and then calling
collectAsState
on them and relying on the equality check to turn false so that you get a new instance, so that the LaunchedEffect key changes, so that it gets recreated sounds like a lot of extra steps that you don’t really want to do when you can simply do what I am doing above. This avoids going from flow into the compose State and then back to collecting a flow and directly takes the flow and collects it in a life-cycle aware way. What are your thoughts on this Csaba?
c

Csaba Szugyiczki

08/26/2022, 11:03 AM
@Stylianos Gakis Your approach absolutely makes sense, and I guess you are right that it might avoid extra unnecessary work. On the other hand I like to keep stuff as simple as possible. There is a lot to unwrap in the code you posted. Seems to be correct, don’t get me wrong, it is a nice solution! So I think if the LoginEvent does not change that often (judging by the name it really shouldn’t) the performance benefit is so small, that I would go with the solution that looks like the other flow-to-state conversions in a Compose app.
s

Stylianos Gakis

08/26/2022, 11:08 AM
Fair points. In my head the repeatOnLifecycle approach feels simpler to reason about. I wasn’t really concerned about performance. Since with this approach I don’t have to think of the equality checks as you also said above. But I can also see how introducing the lifecycle composition local and the repeatOnLifecycle might be more confusing for someone less comfortable with these APIs. So Amr, now you got a couple of alternatives, try them out and report back if you’d like giving us an update on whether this worked for you or not 😊
c

Csaba Szugyiczki

08/26/2022, 11:14 AM
@Stylianos Gakis I think if you wrap your code in a Composable function, then it could be more understandable for outsiders. And if you later find bug or unintended behavior it would be easier to fix/change. Something like this:
Copy code
@Composable
fun <T> FlowCollector(flow: Flow<T>, collector: ((T) -> Unit){
  val lifecycle = LocalLifecycleOwner.current.lifecycle

  LaunchedEffect(flow) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
      flow.collect { event ->
        collector(event)
      }
    }
  }
}
Are you already doing something like this?
a

AmrJyniat

08/26/2022, 11:18 AM
Copy code
sealed interface LoginEvent
data class FailedLoginEvent(val errorMsg: String) : LoginEvent
....
LoginEvent
is normal sealed interface with multiple events type, I'm sending a new event with `sendEvent()`that launch a new coroutine each time, so I'm sure that the problem in the consumer not producer.
s

Stylianos Gakis

08/26/2022, 11:21 AM
No I don’t actually, I’ve been trying lately to model my events as actual state, as suggested in the article I linked above. And in the places where that is not done, I go with the approach I’ve noted above. Your suggestion I believe comes with a bug where if your
collector
changes after the first call, the collect would be referencing a stale parameter, since you’re not keying the LaunchedEffect with it. And with that thought, I think to be technically correct, one would also want to key on the lifecycle too. I don’t know if that ever changes, but if it does that would also be referencing the old reference since in my suggestion that’s not keyed either.
c

Csaba Szugyiczki

08/26/2022, 11:25 AM
@AmrJyniat If you pass two
FailedLoginEvents
both with the same
errorMsg
their equals method will return true, so technically they will be 2 different instances, but representing the same value, so it will not trigger a recomposition. Are you emitting the same error? Could you try emitting two different errors to see if it works?
a

AmrJyniat

08/26/2022, 11:28 AM
Thanks Stylianos, but I think the
uiEvent
gets collected twice in your solution, inside
LaunchEffect()
and when collecting it as a normal flow, what do you think?
@Csaba Szugyiczki you're absolutely right!
s

Stylianos Gakis

08/26/2022, 11:31 AM
The items passed inside
LaunchEffect
are just used as “keys” to know when to restart the
LaunchedEffect
. There is no collection happening there. It’s a way to tell that if somehow the instance of
viewmodel.eventFlow
changes, then this
LaunchedEffect
should restart. In practice maybe this never happens here, but it’s always good to key the things that you’re using inside the
LaunchedEffect
.
c

Csaba Szugyiczki

08/26/2022, 11:33 AM
@AmrJyniat I think you are safe to go with @Stylianos Gakis’s solution. Other solutions would involve bigger changes. (the ones I could think of)
12 Views