I'm working on a Jetpack Compose project where I have a composable function that collects a Flow using collectAsState. I'm facing an issue with handling the initial value in a more idiomatic way. Here's a simplified version of my code:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
FlowExampleTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
NavigationScreen()
}
}
}
}
}
@Composable
fun NavigationScreen(viewModel: MainViewModel = koinViewModel()) {
val destination by viewModel.currentDestination.collectAsState(initial = ScreenName.ScreenOne)
LaunchedEffect(viewModel, destination) {
viewModel.navigateToDeviceSelection(false)
}
LaunchedEffect(key1 = destination) {
Log.e(">> destination", destination.route)
}
Column {
Text(text = destination.route, fontSize = 20.sp)
}
}
MainViewModel.kt
class MainViewModel(
private val navigationHandler: NavigationHandler
) : ViewModel() {
val currentDestination: Flow<NavigationDestination> =
navigationHandler.destination.shareIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
)
fun navigateToDeviceSelection(isValid: Boolean) {
val destination = if (isValid) {
ScreenName.ScreenOne
} else {
ScreenName.ScreenTwo
}
println(">> destination ${destination.route}")
navigationHandler.navigate(destination)
}
}
NavigationDestination
interface NavigationDestination {
val route: String
}
NavigationHandler.kt
interface NavigationHandler {
val destination: SharedFlow<ScreenName>
fun navigate(navigationDestination: ScreenName)
}
Navigator.kt
class Navigator : NavigationHandler {
private val _destination: MutableSharedFlow<ScreenName> = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
override val destination: SharedFlow<ScreenName> = _destination.asSharedFlow()
override fun navigate(navigationDestination: ScreenName) {
_destination.tryEmit(navigationDestination)
}
}
ScreenName
sealed class ScreenName(override val route: String) : NavigationDestination {
object ScreenOne : ScreenName("ScreenOne") {
object ChildOne : ScreenName("ChildOne")
}
object ScreenTwo : ScreenName("ScreenTwo")
}
When I run the code it always print first ScreenOne then move to other condition like ScreenTwo according to condition used inside navigateToDeviceSelection function called. I just give me initial by default because collectAsState need that. I am using SharedFlow because I want to trigger same event repeatedly. Above code is just example of 2 events but actually is more than that.
My main expected output will be to print first according to navigateToDeviceSelection logic not by default value which I provided by collectAsState. I don't want to use null because this example is very basic and I need to add more events in here.
The
LaunchedEffectthat you use to navigate to the desired screen is called asynchronously. There is no guarantee when and even if that effect is executed (although most of the time it will be pretty fast), but in the meantime the current thread continues and needs a destination for theTextcomposable to display. That is why you need the initial value.If you would use a
StateFlowyou could move the entire handling of the navigation (including the initial value that is then set bystateIninstead ofcollectAsState) to the view model. Perhaps that would allow you to restructure your navigation logic so it always provides the desired start screen as the initial value of the flow.I am not sure why you don't want to use a
StateFlowanyways. Yes, it will drop repeated values. But then again, the collected valuedestinationinNavigationScreenis (a delegate to) a Compose State object, so repeatedly changing it to the same value won't trigger a recomposition anyways. If you do not do any fancy stuff with that flow at another location outside of a composable I fail to see the difference.If you want to keep the navigation logic as it is you can always provide an initial value of
nullto the flow to indicate that there is no real value yet. That way you could just skip displaying any screen at all (or display a loading indicator) until a value is supplied.By the way, you should change your collection method to
collectAsStateWithLifecycleto automatically subscribe and unsubscribe the flow according to the current state of the composable. You need the artifactandroidx.lifecycle:lifecycle-runtime-composefor that. Also you need to change the type ofcurrentDestinationtoSharedFlow(orStateFlow, for that matter).