Hi everybody how are you ? I have a problem with ...
# compose
j
Hi everybody how are you ? I have a problem with a mutableStateFlow, when opening another composable it is always being invoked many times this is my code in case you can help me I have been researching for a long time ViewModel
Copy code
private val _marketStatusIsClosed = MutableStateFlow(MarketStatusUiState())
val marketStatusClosed: StateFlow<MarketStatusUiState> get() = _marketStatusIsClosed
Composable
Copy code
val marketStatusState by rememberFlowWithLifecycle(tickerDetailViewModel.marketStatusClosed)
    .collectAsState(MarketStatusUiState())
this is the code that is always running even though it is not visible to the user
Copy code
if (!marketStatusState.isLoading) {
    Log.e("BuyInSharesScreen", "$marketStatusState")
    if (marketStatusState.isMarketClose) {

    } else {
        open another composable
    }
}
the new composable (screen) is being opened many times, and it is already closing so I do the navigation
Copy code
currentBackStackEntry?.arguments?.putParcelable(CREATE_ORDER_INFO, operation)
navigate(Screens.BuyStockConfirmationScreen.route)
c
Hi @Jaime, can you please combine your messages into a single thread? It will be easier to help you
j
yes!
c
How often is the
StateFlow
being updated? If the
value
is updating frequently, but your “market status” state is logically unchanged, you may want to use
distinctUntilChanged()
j
it only updates when you click a button and call an API
I'm going to try the method you sent me thanks @Chris Fillmore
c
So, in fact,
distinctUntilChanged()
has no effect on a StateFlow, sorry I forgot this. But if your StateFlow is being driven by some upstream
Flow
, then applying
distinctUntilChanged()
upstream may help. It’s hard to get a clear picture of what’s going on in your code, without more context.
z
What do you mean by “opening” a composable? If that means making a navigation call, and you're doing that directly in a composition, that's your problem.
j
@Chris Fillmore sorry, I have a screen (composable) has a button that when clicking calls an api
Copy code
Button(
    modifier = Modifier
        .constrainAs(sharesContinueButton) {
            bottom.linkTo(<http://sharesKeyBoard.top|sharesKeyBoard.top>, margin = 20.dp)
            end.linkTo(parent.end)
            start.linkTo(parent.start)
        }
        .padding(end = 20.dp, start = 20.dp),
    onClick = {
        tickerDetailViewModel.marketStatus()
    },
    text = stringResource(id = R.string.continue_),
)
in my viewModel I have something like
Copy code
fun marketStatus() {
    viewModelScope.launch {
        val marketStatusResult = tickerDetailMarketStatusUseCase.run(UseCase.None())
        marketStatusResult collectAsSuccess {
            Log.e("marketStatus", "collectAsSuccess")
            _marketStatusIsClosed.value = if (it.status == MARKET_STATUS_CLOSED) {
                MarketStatusUiState(
                    isLoading = false,
                    isMarketClose = false
                )
            } else {
                MarketStatusUiState(
                    isLoading = false,
                    isMarketClose = false
                )
            }
        } collectAsFailure {
            Log.e("marketStatus", "collectAsFailure")
            _marketStatusIsClosed.value = MarketStatusUiState(
                isLoading = false,
                isMarketClose = true
            )
        }
    }
}
this is my variables observer in viewmodel
Copy code
private val _marketStatusIsClosed = MutableStateFlow(MarketStatusUiState())
val marketStatusClosed: StateFlow<MarketStatusUiState> get() = _marketStatusIsClosed
in my composable i have this validation
Copy code
if (!marketStatusState.isLoading) {
    Log.e("BuyInSharesScreen", "$marketStatusState")
    if (marketStatusState.isMarketClose) {
        //TODO: change strings
        HapiDialog(
            dialog = HapiDialogModel(
                content = HapiDialogContentModel(
                    titleId = R.string.signup_message_email_already_registered,
                    buttonTitleId = listOf(R.string.login),
                    resourceId = R.drawable.ic_dialog_error,
                ),
                type = HapiDialogModel.HapiDialogType.MIDDLE,
                resourceType = HapiDialogModel.HapiDialogResourceType.IMAGE,
                quantityButtons = 1
            )
        )
    } else {
        operationDetailListener(
            CreateOrderInfo(
                shares = stockOptionWithoutFormat.toDouble(),
                buyingPower = buyingPowerState.buyingPower,
                tickerCompanyName = tickerCompanyName,
                currentPrice = marketPriceState.price,
                estimatedCost = estimatedCostState,
                tickerCompany = ticker
            )
        )
    }
}
go into the else and call the operationDetailListener method
Copy code
navController.navigateToOperationDetailConfirm(operation)
this is my method navigateToOperationDetailConfirm
Copy code
fun NavHostController.navigateToOperationDetailConfirm(operation: CreateOrderInfo) {
    currentBackStackEntry?.arguments?.putParcelable(CREATE_ORDER_INFO, operation)
    navigate(Screens.BuyStockConfirmationScreen.route)
}
open new screen and and the state of marketStatusState is being called several times and the new compsable is opened many times
@Zach Klippenstein (he/him) [MOD] yes call navigation in composable
do you have any idea how not to call it in composable?
z
This is discussed extensively on this channel and mentioned in the official docs as well:
You should only call 
navigate()
 as part of a callback and not as part of your composable itself, to avoid calling 
navigate()
 on every recomposition.
Typically navigation should be done as part of an event handler, not as a result of composition.
So in this case, if your view model is setting that state value, then it should probably be making navigation calls in the same place as it changes that state.
j
@Zach Klippenstein (he/him) [MOD] you have example ?
I am calling navigate in the NavHost is correct call in this place ?
z
This is generally a bad idea, but if you really did need to make a navigate call from a composable you’d need to put it in a
DisposableEffect
or
LaunchedEffect
j
@Zach Klippenstein (he/him) [MOD] thanks, but what is the best way to do it do you have an example?
z
Without seeing more of your code, i can’t give a specific example
j
Can I send it to you by direct message?
z
The usual example is that if navigation is triggered by a button click, then put the navigation call in the click event
sure
j
Yes I have followed those examples but what I have to do is execute the navigation when it receives the response from an api
z
Hm, good question. I’m actually not sure since it seems like compose navigation forces you to create your nav controller in a composable, which means it’s awkward to pass back to your viewmodel. I haven’t used Jetpack Nav at all myself so i’m not sure what the best practice is here.
Ok so i asked around a bit and it seems that there’s no really good answer to this right now. Some options are: 1. model more of what the ViewModel is doing as state and maybe don’t rely on Jetpack Navigation for navigation at all (something like https://github.com/rjrjr/compose-backstack would be a more natural fit here since it would let your ViewModel be the only source-of-truth for navigation) 2. Expose an event flow from your view model, which your composable can collect in a LaunchedEffect and make navigation calls for. There are a lot of gotchas here.
j
@Zach Klippenstein (he/him) [MOD] thanks 😄
i
Yep, if you want to process events in Compose, you should be collecting an event Flow in a
LaunchedEffect
, not using state changes as some proxy for events
Although note that the simple change of moving your processing into a
LaunchedEffect
is enough to only process that 'event' once
Copy code
} else {
    // Make this only happen once
    LaunchedEffect(marketStatusState) {
        operationDetailListener(
            CreateOrderInfo(
                shares = stockOptionWithoutFormat.toDouble(),
                buyingPower = buyingPowerState.buyingPower,
                tickerCompanyName = tickerCompanyName,
                currentPrice = marketPriceState.price,
                estimatedCost = estimatedCostState,
                tickerCompany = ticker
            )
        )
    }
}
Note that you also have a signal `v`f`n`you've called navigate already
try
(the one passed to your
composable
method) has a
lifecycle.currentState == Lifecycle.State.RESUMED
- when you call
navigate()
, your state is synchronously moved down immediately. That doesn't change the fact that you shouldn't call
startActivity
or
navigate
or any other side effect as part of composition (you still need to fix that), but it does give you a way of ignoring events that happen after you've already started navigating (and crossfading/animating to the new screen)