Jetpack Compose Navigation loads screen infinitely - android

I am trying to implement Navigation using single activity and
multiple Composable Screens.
This is my NavHost:
#Composable
#ExperimentalFoundationApi
fun MyNavHost(
modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController(),
startDestination: String = HOME.route,
viewModelProvider: ViewModelProvider,
speech: SpeechHelper
) = NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(route = HOME.route) {
with(viewModelProvider[HomeViewModel::class.java]) {
HomeScreen(
speech = speech,
viewModel = this,
modifier = Modifier.onKeyEvent { handleKeyEvent(it, this) }
) {
navController.navigateTo(it)
}
}
}
composable(route = Destination.VOLUME_SETTINGS.route) {
VolumeSettingsScreen(
viewModelProvider[VolumeSettingsViewModel::class.java]
) { navController.navigateUp() }
}
}
fun NavHostController.navigateTo(
navigateRoute: String,
willGoBackTo: String = HOME.route
): Unit = navigate(navigateRoute) {
popUpTo(willGoBackTo) { inclusive = true }
}
My screen looks like this:
#Composable
fun HomeScreen(
speech: SpeechHelper,
viewModel: HomeViewModel,
modifier: Modifier,
onNavigationRequested: (String) -> Unit
) {
MyBlindAssistantTheme {
val requester = remember { FocusRequester() }
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
initialValue = UiState.Speak(
R.string.welcome_
.withStrResPlaceholder(R.string.text_home_screen)
.toSpeechUiModel()
)
)
uiState?.let {
when (it) {
is UiState.Speak -> speech.speak(it.speechUiModel)
is UiState.SpeakRes -> speech.speak(it.speechResUiModel.speechUiModel())
is UiState.Navigate -> onNavigationRequested(it.route)
}
}
Column(
modifier
.focusRequester(requester)
.focusable(true)
.fillMaxSize()
) {
val rowModifier = Modifier.weight(1f)
Row(rowModifier) {...}
}
LaunchedEffect(Unit) {
requester.requestFocus()
}
}
}
This is the ViewModel:
class HomeViewModel : ViewModel() {
private val mutableUiState: MutableStateFlow<UiState?> = MutableStateFlow(null)
val uiState = mutableUiState.asStateFlow()
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(Destination.VOLUME_SETTINGS.route.toNavigationState())
}
}
When a button is clicked the ViewModel is called and the NavigateUiState is emitted... but it keeps being emitted after the next screen is loaded and that causes infinite screen reloading. What should be done to avoid this?

I re-implemented your posted code with 2 screens, HomeScreen and SettingScreen and stripped out some part of the UiState class and its usages.
The issue is in your HomeScreen composable, not in the StateFlow emission.
You have this mutableState
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
initialValue = UiState.Speak
)
that is being observed in one of your when block that executes a navigation callback.
uiState?.let {
when (it) {
is UiState.Navigate -> {
onNavigationRequested(it.route)
}
UiState.Speak -> {
Log.d("UiState", "Speaking....")
}
}
When your ViewModel function is called
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(UiState.Navigate(Destination.SETTINGS_SCREEN.route))
}
it will update uiState, setting its value to Navigate, observed by HomeScreen, satisfies the when block and then triggers the callback to navigate to the next screen.
Now based on the official Docs,
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.
but in your case, the navigation is triggered by an observed mutableState, and the mutableState is part of your HomeScreen composable.
It seems like when the navController performs a navigation and the NavHost being a Composable
#Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) { ... }
it will execute a re-composition, because of it, it will call again the HomeScreen (HomeScreen is not re-composed, its state remains the same) and because the HomeScreen's UiState value is still set to Navigate, it satisfies the when block, triggers again the callback to navigate, and NavHost re-composes, an infinite cycle is then created.
What I did (and its very ugly) is I created a boolean flag inside the viewModel, used it to wrap the callback conditionally,
uiState?.let {
when (it) {
is UiState.Navigate -> {
if (!viewModel.navigated) {
onNavigationRequested(it.route)
viewModel.navigated = true
} else {
// dirty empty else
}
}
UiState.Speak -> {
Log.d("UiState", "Speaking....")
}
}
}
and setting it to true afterwards, preventing the cycle.
I can hardly guess your compose implementation structure but I usually don't mix my one-time event actions and UiState, instead I have a separate UiEvent sealed class that will group "one-time" events such as the following:
Snackbar
Toast
Navigation
and having them emitted as a SharedFlow emissions because these events doesn't need any initial state or initial value.
Continuing further, I created this class
sealed class UiEvent {
data class Navigate(val route: String) : UiEvent()
}
use it in the ViewModel as a type (Navigate in this case),
private val _event : MutableSharedFlow<UiEvent> = MutableSharedFlow()
val event = _event.asSharedFlow()
fun onNavigateButtonClicked(){
viewModelScope.launch {
_event.emit(UiEvent.Navigate(Destination.SETTINGS_SCREEN.route))
}
}
and observe it in HomeScreen this way via LaunchedEffect, triggering the navigation in it without the callback being bound to any observed state.
LaunchedEffect(Unit) {
viewModel.event.collectLatest {
when (it) {
is UiEvent.Navigate -> {
onNavigationRequested(it.route)
}
}
}
}
This approach doesn't introduce the infinite navigation cycle and the dirty boolean checking is not needed anymore.
Also have a look this S.O post, similar to your case

Related

Jetpack compose and viewModels - Ignore last state value on first composition

How do i ignore the viewModel's last state on the first composition of a composable?
The usecase is the following:
User enters MyDumbComposable
Clicks to add a song to a playlist
When the action is successful, popBack
This is my viewModel
#HiltViewModel
class AddToPlaylistViewModel #Inject constructor(
private val addToPlaylistUseCase: AddToPlaylistUseCase,
) : ViewModel() {
private val _state = mutableStateOf<AddToPlaylistState>(AddToPlaylistState.Initial)
val state: State<AddToPlaylistState> = _state
operator fun invoke(
params: AddToPlaylistParams
) {
addToPlaylistUseCase(params)
.flowOn(Dispatchers.IO)
.onEach { _state.value = it }
.launchIn(viewModelScope)
}
}
This is MyDumbComposable
fun MyDumbComposable(
addToPlaylistViewModel: AddToPlaylistViewModel = hiltViewModel(),
song: Song,
popBack: () -> Unit
) {
if (addToPlaylistViewModel.state.value is AddToPlaylistState.Loaded) {
LaunchedEffect(Unit) {
popBack()
}
}
fun onClick(playlist: PlaylistWithSongs) {
addToPlaylistViewModel(
AddToPlaylistParams(
selected = Selected(listOf(song)),
playlist = playlist.playlist
)
)
}
///...
It works the first time and pops correctly.
However, whenever the user returns to that
composable, the AddToPlaylistViewModel is cached
so the last value is still AddToPlaylistState.Loaded,
meaning it'll pop the screen right away.
I ended up resetting my state using a DisposableEffect:
DisposableEffect(Unit) {
onDispose {
addToPlaylistViewModel.resetState()
}
}

Passing State value, or State, as Composable function parameter

In a Composable function, I can pass as parameter the State, or the value of the State. Any reason for preferring to pass the value of the State, instead of the State?
In both cases, the composable is stateless, so why should I distinguish both cases?
It's possible to pass state's value. For example:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val isLoading = mutableStateOf(false)
val onClickAtButton = {
lifecycleScope.launch(Dispatchers.Main) {
isLoading.value = true
withContext(Dispatchers.IO) {
//Do some heavy operation live REST call
}
isLoading.value = false
}
}
setContent {
MyComposable(isLoading.value, onClickAtButton)
}
}
}
#Composable
fun MyComposable(
isLoading: Boolean = false,
onClickAtButton: () -> Unit = {}
){
Box(modifier = Modifier.fillMaxSize(){
Button(onClick = onClickAtButton)
if(isLoading){
CircularProgressIndicator()
}
}
}
Hope it helps somebody.
There is a slight difference between passing State or just the value of a State regarding recomposition.
Let's start with passing State:
#Composable
fun Example1(text: State<String>) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: State<String>) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text.value)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
On first start you will see the log output
Example1 recomposition
Example2 recomposition
However when you click the button, you will only see an additional
Example2 recomposition
Because you're passing down State and only Example2 is reading the state, Example1 does not need to be recomposed.
Let's change the parameters to a plain type:
#Composable
fun Example1(text: String) {
SideEffect { Log.d("Example", "Example1 recomposition") }
Example2(text)
}
#Composable
fun Example2(text: String) {
SideEffect { Log.d("Example", "Example2 recomposition") }
Text(text)
}
#Composable
fun Screen() {
val text = remember { mutableStateOf("hello") } }
Example1(text.value)
Button(
onClick = { text.value = "world" }
) {
Text("Click me")
}
}
When you click the button now, you will see two additional lines in the log output
Example1 recomposition
Example2 recomposition
Since text is now a plain type of the function signatures of both composables, both need to be recomposed when the value changes.
However always passing down State can become quite cumbersome. Compose is quite good at detecting what needs to be recomposed so this should be considered a micro optimization. I just wanted to point out that there is a slight difference which every developer using Compose should know about.

Jetpack Compose application-wide conditional TopAppBar best practice

I have an Android Jetpack Compose application that uses BottomNavigation and TopAppBar composables. From the tab opened via BottomNavigation users can navigate deeper into the navigation graph.
The problem
The TopAppBar composable must represent the current screen, e.g. display its name, implement some options that are specific to the screen opened, the back button if the screen is high-level. However, Jetpack Compose seems to have no out-of-the-box solution to that, and developers must implement it by themselves.
So, obvious ideas come with obvious drawbacks, some ideas are better than others.
The baseline for tracking navigation, as suggested by Google (at least for BottomNavigation), is a sealed class containing objects that represent the current active screen. Specifically for my project, it's like this:
sealed class AppTab(val route: String, #StringRes val resourceId: Int, val icon: ImageVector) {
object Events: AppTab("events_tab", R.string.events, Icons.Default.EventNote)
object Projects: AppTab("projects_tab", R.string.projects, Icons.Default.Widgets)
object Devices: AppTab("devices_tab", R.string.devices, Icons.Default.DevicesOther)
object Employees: AppTab("employees_tab", R.string.employees, Icons.Default.People)
object Profile: AppTab("profile_tab", R.string.profile, Icons.Default.AccountCircle)
}
Now the TopAppBar can know what tab is opened, provided we remember the AppTab object, but how does it know if a screen is opened from within a given tab?
Solution 1 - obvious and obviously wrong
We provide each screen its own TopAppBar and let it handle all the necessary logic. Aside from a lot of code duplication, each screen's TopAppBar will be recomposed on opening the screen, and, as described in this post, will flicker.
Solution 2 - not quite elegant
From now on I decided to have a single TopAppBar in my project's top level composable, that will depend on a state with current screen saved. Now we can easily implement logic for Tabs.
To solve the problem of screens opened from within a Tab, I extended Google's idea and implemented a general AppScreen class that represents every screen that can be opened:
// This class represents any screen - tabs and their subscreens.
// It is needed to appropriately change top app bar behavior
sealed class AppScreen(#StringRes val screenNameResource: Int) {
// Employee-related
object Employees: AppScreen(R.string.employees)
object EmployeeDetails: AppScreen(R.string.profile)
// Events-related
object Events: AppScreen(R.string.events)
object EventDetails: AppScreen(R.string.event)
object EventNew: AppScreen(R.string.event_new)
// Projects-related
object Projects: AppScreen(R.string.projects)
// Devices-related
object Devices: AppScreen(R.string.devices)
// Profile-related
object Profile: AppScreen(R.string.profile)
}
I then save it to a state in the top-level composable in the scope of TopAppBar and pass currentScreenHandler as an onNavigate argument to my Tab composables:
var currentScreen by remember { mutableStateOf(defaultTab.asScreen()) }
val currentScreenHandler: (AppScreen) -> Unit = {navigatedScreen -> currentScreen = navigatedScreen}
// Somewhere in the bodyContent of a Scaffold
when (currentTab) {
AppTab.Employees -> EmployeesTab(currentScreenHandler)
// And other tabs
// ...
}
And from inside the Tab composable:
val navController = rememberNavController()
NavHost(navController, startDestination = "employees") {
composable("employees") {
onNavigate(AppScreen.Employees)
Employees(it.hiltViewModel(), navController)
}
composable("employee/{userId}") {
onNavigate(AppScreen.EmployeeDetails)
Employee(it.hiltViewModel())
}
}
Now the TopAppBar in the root composable knows about higher-level screens and can implement necessary logic. But doing this for every subscreen of an app? A considerable amount of code duplication, and architecture of communication between this app bar and a composable it represents (how the composable reacts to actions performed on the app bar) is yet to be composed (pun intended).
Solution 3 - the best?
I implemented a viewModel for handling the needed logic, as it seemed like the most elegant solution:
#HiltViewModel
class AppBarViewModel #Inject constructor() : ViewModel() {
private val defaultTab = AppTab.Events
private val _currentScreen = MutableStateFlow(defaultTab.asScreen())
val currentScreen: StateFlow<AppScreen> = _currentScreen
fun onNavigate(screen: AppScreen) {
_currentScreen.value = screen
}
}
Root composable:
val currentScreen by appBarViewModel.currentScreen.collectAsState()
But it didn't solve the code duplication problem of the second solution. First of all, I had to pass this viewModel to the root composable from MainActivity, as there appears to be no other way of accessing it from inside a composable. So now, instead of passing a currentScreenHandler to Tab composables, I pass a viewModel to them, and instead of calling the handler on navigate event, I call viewModel.onNavigate(AppScreen), so there's even more code! At least, I maybe can implement a communication mechanism mentioned in the previous solution.
The question
For now the second solution seems to be the best in terms of code amount, but the third one allows for communication and more flexibility down the line for some yet to be requested features. I may be missing something obvious and elegant. Which of my implementations you consider the best, and if none, what would you do to solve this problem?
Thank you.
I use a single TopAppBar in the Scaffold and use a different title, drop-down menu, icons, etc by raising events from the Composables. That way, I can use just a single TopAppBar with different values. Here is an example:
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
var appTitle by remember { mutableStateOf("") }
var showFab by remember { mutableStateOf(false) }
var showDropdownMenu by remember { mutableStateOf(false) }
var dropdownMenuExpanded by remember { mutableStateOf(false) }
var dropdownMenuName by remember { mutableStateOf("") }
var topAppBarIconsName by remember { mutableStateOf("") }
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val tourViewModel: TourViewModel = viewModel()
val clientViewModel: ClientViewModel = viewModel()
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = "Back Arrow"
)
}
}
} else {
{
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.apply {
if (isClosed) open() else close()
}
}
}) {
Icon(Icons.Filled.Menu, contentDescription = null)
}
}
}
Scaffold(
scaffoldState = scaffoldState,
drawerContent = {
DrawerContents(
navController,
onMenuItemClick = { scope.launch { scaffoldState.drawerState.close() } })
},
topBar = {
TopAppBar(
title = { Text(appTitle) },
navigationIcon = navigationIcon,
elevation = 8.dp,
actions = {
when (topAppBarIconsName) {
"ClientDirectoryScreenIcons" -> {
// search icon on client directory screen
IconButton(onClick = {
clientViewModel.toggleSearchBar()
}) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Search Contacts"
)
}
}
}
if (showDropdownMenu) {
IconButton(onClick = { dropdownMenuExpanded = true }) {
Icon(imageVector = Icons.Filled.MoreVert, contentDescription = null)
DropdownMenu(
expanded = dropdownMenuExpanded,
onDismissRequest = { dropdownMenuExpanded = false }
) {
// show different dropdowns based on different screens
when (dropdownMenuName) {
"ClientDirectoryScreenDropdown" -> ClientDirectoryScreenDropdown(
onDropdownMenuExpanded = { dropdownMenuExpanded = it })
}
}
}
}
}
)
},
...
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
NavHost(
navController = navController,
startDestination = Screen.Tours.route
) {
composable(Screen.Tours.route) {
TourScreen(
tourViewModel = tourViewModel,
onSetAppTitle = { appTitle = it },
onShowDropdownMenu = { showDropdownMenu = it },
onTopAppBarIconsName = { topAppBarIconsName = it }
)
}
Then set the TopAppBar values from different screens like this:
#Composable
fun TourScreen(
tourViewModel: TourViewModel,
onSetAppTitle: (String) -> Unit,
onShowDropdownMenu: (Boolean) -> Unit,
onTopAppBarIconsName: (String) -> Unit
) {
LaunchedEffect(Unit) {
onSetAppTitle("Tours")
onShowDropdownMenu(false)
onTopAppBarIconsName("")
}
...
Not probably the perfect way of doing it, but no duplicate code.

Can I replace produceState with mutableStateOf in the Compose sample project?

The following Code A is from the project.
uiState is created by the delegate produceState, can I use mutableStateOf instead of produceState? If so, how can I write code?
Why can't I use Code B in the project?
Code A
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val uiState by produceState(initialValue = DetailsUiState(isLoading = true)) {
val cityDetailsResult = viewModel.cityDetails
value = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
}
when {
uiState.cityDetails != null -> {
...
}
#HiltViewModel
class DetailsViewModel #Inject constructor(
private val destinationsRepository: DestinationsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val cityName = savedStateHandle.get<String>(KEY_ARG_DETAILS_CITY_NAME)!!
val cityDetails: Result<ExploreModel>
get() {
val destination = destinationsRepository.getDestination(cityName)
return if (destination != null) {
Result.Success(destination)
} else {
Result.Error(IllegalArgumentException("City doesn't exist"))
}
}
}
data class DetailsUiState(
val cityDetails: ExploreModel? = null,
val isLoading: Boolean = false,
val throwError: Boolean = false
)
Code B
#Composable
fun DetailsScreen(
onErrorLoading: () -> Unit,
modifier: Modifier = Modifier,
viewModel: DetailsViewModel = viewModel()
) {
val cityDetailsResult = viewModel.cityDetails
val uiState=if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
...
uiState is created by the delegate produceState, can I use mutableStateOf instead of produceState? If so, how can I write code?
No, you can't write it using the mutableStateOf (direct initialization not possible). In order to understand why it not possible we need to understand the use of produceState
According to documentation available here
produceState launches a coroutine scoped to the Composition that can
push values into a returned State. Use it to convert non-Compose state
into Compose state, for example bringing external subscription-driven
state such as Flow, LiveData, or RxJava into the Composition.
So basically it is compose way of converting non-Compose state to compose the state.
if you still want to use mutableStateOf you can do something like this
var uiState = remember { mutableStateOf(DetailsUIState())}
LaunchedEffect(key1 = someKey, block = {
uiState = if (cityDetailsResult is Result.Success<ExploreModel>) {
DetailsUiState(cityDetailsResult.data)
} else {
DetailsUiState(throwError = true)
}
})
Note: here someKey might be another variable which handles the recomposition of the state
What is wrong with this approach?
As you can see it's taking another variable someKey to recomposition. and handling it is quite tough compared to produceState
Why can't I use Code B in the project?
The problem with code B is you don't know whether the data is loaded or not while displaying the result. It's not observing the viewModel's data but its just getting the currently available data and based on that it gives the composition.
Imagine if the viewModel is getting data now you will be having UiState with isLoading = true but after some time you get data after a successful API call or error if it fails, at that time the composable function in this case DetailsScreen doesn't know about it at all unless you are observing the Ui state somewhere above this composition and causing this composition to recompose based on newState available.
But in produceState the state of the ui will automatically changed once the suspended network call completes ...

Jetpack Compose & Navigation: Problems share ViewModel in nested graph

According to this example I implemented shared viewModels in a nested navigation graph.
Setup
Nested Graph:
private fun NavGraphBuilder.accountGraph(navController: NavHostController) {
navigation(
startDestination = "main",
route = "account") {
composable("main") {
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
composable("login") {
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
}
}
NavHost:
#Composable
private fun NavHost(navController: NavHostController, modifier: Modifier = Modifier){
NavHost(
navController = navController,
startDestination = MainScreen.Home.route,
modifier = modifier
) {
composable("home") { HomeScreen(hiltViewModel()) }
composable("otherRoute") { OtherScreen(hiltViewModel()) }
accountGraph(navController)
}
}
BottomNavBar:
#Composable
private fun ButtonNav(navController: NavHostController) {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { ... },
label = { ... },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) { saveState = true }
}
// Avoid multiple copies of the same destination when
// re-selecting the same item
launchSingleTop = true
// Restore state when re-selecting a previously selected item
restoreState = true
}
}
)
}
}
}
Problem
With this setup if I naviagte to "account" (the nested graph) and back to any other route I get the error:
java.lang.IllegalArgumentException: No destination with route account is on the NavController's back stack. The current destination is Destination(0x78dd8526) route=otherRoute
Assumptions / Research Results
BottomNavItem
The exception did not occure when I remove the popUpTo(route) onClick. But then I ended up with a large stack.
lifecycle of backStackEntry
Have a look at the following:
//...
composable("main") { backStackEntry ->
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
//...
I found out when navigating back the composable which will be left will be recomposed but in this case the backStackEntry seams to have another lifecycle.currentState because if I wrap the whole composable like this:
//...
composable("main") { backStackEntry ->
if(backStackEntry.lifecycle.currentState == Lifecycle.State.RESUMED){
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
}
//...
... the exception did not occure.
The idea with the lifecycle issue came into my mind when I saw that the offical example has similar workarounds in place.
Summary
I actually do not know if I did something wrong or if I miss a conecept here. I can put the lifecycle-check-workaround into place but is this really as intended? Additional to that I did not find any hint in the doc regarding that.
Does anybody know how to fix that in a proper way?
Regards,
Chris
This is how you do it now but make sure you have the latest compose navigation artefacts:
private fun NavGraphBuilder.accountGraph(navController: NavHostController) {
navigation(
startDestination = "main",
route = "account") {
composable("main") {
val parentEntry = remember {
navController.getBackstackEntry("account")
}
val vm = hiltViewModel<AccountViewModel(parentEntry)
//... ui ...
}
composable("login") {
val parentEntry = remember {
navController.getBackstackEntry("account")
}
val vm = hiltViewModel<AccountViewModel(parentEntry)
//... ui ...
}
}
}
There was an issue with the navigation component. It has been fixed for me with v2.4.0-alpha08

Categories

Resources