https://kotlinlang.org logo
#compose
Title
# compose
a

Alexa_Gal

01/29/2021, 6:33 PM
Hi guys, not sure if this belong to this channel but ill try. im using Compose + StateFlow and i have a function that will trigger an event (i’ll call it
authMethod
)
Copy code
viewModel.authMethod.onEach {
            Log.e("i'm here", "yes im here ${it}")
        }.collect()
this will be trigger the first time and every time after i change the value on the viewModel. my issue is that when i push new view (using
Navigation alpha06
) and i pop back the
onEach
will be triggered again. How could i only tell
authMethod
to be triggered only on changes and not every-time the compose is rendering ?🌈 With liveData i had the observable that only triggers on changes, but i can not find the equivalent on StateFlow :/
z

Zach Klippenstein (he/him) [MOD]

01/29/2021, 6:35 PM
where are you calling
collect()
from?
LaunchedEffect
?
a

Alexa_Gal

01/29/2021, 6:36 PM
yes, it looks like this
Copy code
val coroutineScope = rememberCoroutineScope()

    LaunchedEffect(coroutineScope) {
        viewModel.authMethod.onEach {
            Log.e("ime here", "yes im here ${it}")
        }.launchIn(coroutineScope)
    }
j

Joost Klitsie

01/29/2021, 7:20 PM
I think viewModel.authMethod.collectAsState() is more convenient. But anyway, i do think this is what you can expect. Reobserving the stateflow will trigger the oneach, same as you would go back and forth between fragments and recreating the view also triggers the livedata observers. I think you should not observe on an event like that, perhaps you should create some sort of event stateflow that resets when the value has been read, or make the auth a part of the state of the screen.
z

Zach Klippenstein (he/him) [MOD]

01/29/2021, 7:23 PM
So you don’t need to use both
rememberCoroutineScope
and
LaunchedEffect
- that’s redundant, use one or the other. The parameter to
LaunchedEffect
should be a thing that will cause the coroutine to restart if it changes (i.e. the data dependency of the coroutine). This could just be
Copy code
LaunchedEffect(viewModel.authMethod) {
  viewModel.authMethod.collect {
    Log.e("ime here", "yes im here $it")
  }
}
2
Hm, that doesn’t explain why it’s being restarted though. Where is this code? If it’s on the screen you’re navigating away from and back to, then I would expect the coroutine to be cancelled when you navigate away, and restarted when you navigate back to it, but not restarted on every composition while you’re on that screen.
j

Joost Klitsie

01/29/2021, 7:49 PM
I think it is triggered because it is restarted, as she wrote that it happens when navigating back and forth
So it should be treated either as an event
Or it should be taken out to the parent (which doesnt change)
a

Alexa_Gal

01/29/2021, 7:59 PM
That's correct joost it happens on the popBack Screen A prints message frist time Screen A does some network change on the viewModel and it triggers again the message (all good here) Then screen A pushes screen B Screen B popback and message is printed (this is the event I don't want to be teiggerd when screen B is popBack
Whit liveData works fine using observable it only gets trigger when viewmodel changes the value. But I can't find the equivalent on Flow
a

Adam Powell

01/30/2021, 3:58 PM
This looks like you want to hoist this responsibility into your viewmodel or similar. But with regard to the linked snippet above,
It's important to avoid writing code like this in a
@Composable
function:
Copy code
// fire a one-off event to get the recipe from api
val onLoad = viewModel.onLoad.value
if (!onLoad){
    viewModel.onLoad.value = true
    viewModel.onTriggerEvent(RecipeEvent.GetRecipeEvent(recipeId))
}
Here are several reasons why:
Composition is transactional. When the composable function executes, its effects have not been applied yet. If composition fails, or conflicts with another parallel recomposition (this is going to start happening soon!) then its changes will not be applied and it is if it never happened.
This is why it's important that all side effects use the
*Effect
APIs:
SideEffect
,
DisposableEffect
,
LaunchedEffect
. The
viewModel.onTriggerEvent
call is an unsafe side effect as written.
The contents of those three effect API calls do not run until composition is confirmed as successful
and they are run on the main thread, so you will have far fewer threading questions to answer in the future.
There's also no reason for "first-view" policy enforcement to be handled in composition vs. in the viewmodel class itself, and moving it there makes the behavior simpler and easier to test. Consider an alternative like the following:
Copy code
// In composition
SideEffect {
    viewModel.onComposed(recipeId)
}
This places responsibility for any deduplication or policy enforcement on the viewmodel class instead, where it is much easier to confirm and test. It makes the composable function much simpler too!
This is also in keeping with the Law of Demeter: the composable function is only responsible for presenting the contents of the viewmodel. The act of sending an event the first time that content is successfully presented (and tracking the meaning and scope of, "first") is an implementation detail of the viewmodel (or something even further upstream in your overall data model), not the composable function.
☝🏼 2