I'm using NavHost and a NavHostController to navigate in my Jetpack Compose application. To specify destinations, I use string-based routes:
NavHost(
navController,
startDestination = "FOO"
) {
composable("FOO") {
Foo()
}
composable("BAR") {
Bar()
}
}
I would like to retrieve the current route as a string (FOO or BAR) using the navController. The closest alternative I can find is navController.currentDestination.id that, however, returns a numeric ID, not the route as a string.
Is there any way to retrieve this value?
I'm using Jetpack Compose beta01 and Compose Navigation alpha08.
From androidx.navigation:navigation-compose version 2.4.0-alpha01, androidx.navigation.compose.KEY_ROUTE is replaced with navBackStackEntry?.destination?.route.
The KEY_ROUTE argument has been replaced with the route property on NavDestination, allowing you to call navBackStackEntry.destination.route directly.
Release Note for 2.4.0-alpha01
Example code:
val currentRoute = navController.currentBackStackEntry?.destination?.route
Or, if you need to observe the navigation:
val navBackStackEntry by navController.currentBackStackEntryAsState()
when (val currentRoute = navBackStackEntry?.destination?.route) {
// do something with currentRoute
}
Resolved!
import androidx.navigation.compose.KEY_ROUTE
val currentRoute = navController.currentBackStackEntry?.arguments?.getString(KEY_ROUTE)
Not the cleanest solutions IMO, but it is used in the official Android documentation for navigating with compose: https://developer.android.com/jetpack/compose/navigation?hl=it
For those who have a problem with getting a route from NavDestination object, such as me. Check the class type. There are two classes of NavDestination.kt and NavDestination.java. Cast it to NavDestination.kt to be able to get the route value.
Related
I have a Screen ( composable function ) that gets It's data from view model ( a list and two function to remove and add data in it ).
#Composable
fun MainScreen(
notes: List<Note>,
onAddNote: (Note) -> Unit,
onRemoveNote: (Note) -> Unit
){}
Now when i call this function inside the composable of my Nav host, I get errors stating that i should fill the parameters.
#Composable
fun NotesNavigation(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Navigation.MainScreen.name){
composable(Navigation.MainScreen.name){
MainScreen() // error here
}
}
}
Now I am wondering what is the best practice to sort it out, do i need to provide default values for my parameters like supplying an empty list
or
there is better way to get around it.
You can set default values to the MainScreenFunction, but since you are using navigation, this would become useless. I would suggest to set the viewmodel as a parameter. The viewmodel should still be passed through the navigation though.
I don't know if you use any dependency injection. If so, that would make it a bit easier. Then you can set it up like this:
#Composable
fun MainScreen(
viewModel: MainScreenViewModel = getViewModel() //If using Koin DI
){
...
}
This way, the navigation doesn't have to know about the viewmodel. You can still set it though, if you do need a different viewModel then the one injected for example.
I have a single activity app using only composables for the ui (one activity, no fragments). I use one viewmodel to keep data for the ui in two different screens (composables). I create the viewmodel in both screens as described in state documentation
#Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel())
Now I noticed that the data that was loaded or set in the first screen is reset in the second.
I also noticed that init{} is called every time viewModel() is called. Is this really the expected behavior?
According to the method's documentation it should return either an existing ViewModel or create a new one.
I also see that the view models are different objects. So viewModel() always creates a new one. But why?
Any ideas what I could be doing wrong? Or do I misunderstand the usage of the method?
Usually view model is shared for the whole composables scope, and init shouldn't be called more than once.
But if you're using compose navigation, it creates a new model store owner for each destination. If you need to share models between destination, you can do it like in two ways:
By passing it directly to viewModel call. In this case only the passed view model will be bind to parent store owner, and all other view models created inside will be bind(and so destroyed when route is removed from the stack) to current route.
By proving value for LocalViewModelStoreOwner, so all composables inside will be bind to the parent view model store owner, and so are not gonna be freed when route is removed from the stack.
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
val model = viewModel<Model>(viewModelStoreOwner = viewModelStoreOwner)
}
composable("second") {
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
val model = viewModel<Model>()
}
}
}
Before I use a navigation graph in my Android Studio project, just like the article says.
I use an xml file located in res\navigation folder to include all my different destinations.
At present, I'm learning Jetpack Compose Navigation by the article.
Code A is from the official sample project mentioned in the above article.
It seems that Jetpack Compose Navigation use Code A and other codes to navigate, and I can't find any XML file in res\navigation folder.
1: Is the navigation graph obsoleted when I use Jetpack Compose Navigation ?
2: Don't I need to use the navigation graph located in res\navigation folder again when I use Jetpack Compose Navigation?
Code A
#Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = Overview.name,
modifier = modifier
) {
composable(Overview.name) {
OverviewBody(
onClickSeeAllAccounts = { navController.navigate(Accounts.name) },
onClickSeeAllBills = { navController.navigate(Bills.name) },
onAccountClick = { name ->
navigateToSingleAccount(navController, name)
},
)
}
composable(Accounts.name) {
AccountsBody(accounts = UserData.accounts) { name ->
navigateToSingleAccount(navController = navController, accountName = name)
}
}
composable(Bills.name) {
BillsBody(bills = UserData.bills)
}
...
}
}
Navigation has always had three ways of building a NavGraph object:
Manually, by using the NavGraph constructor itself. While this serves as the basis for all other methods listed here, you aren't meant to use these APIs directly.
Building the graph via Navigation XML. This is a way of building a graph at compile time using the Navigator Editor tooling and the Safe Args plugin. This method only supports Navigation with Fragments.
Using the Navigation Kotlin DSL. This provides a type-safe way of building a navigation graph programmatically, at runtime by using a Kotlin DSL. This method supports both Navigation with Fragments and Navigation Compose.
As Compose is a way of programmatically building your UI, Navigation only supports the programmatic version of building the graph - via that Kotlin DSL that is exactly what that trailing lambda of NavHost provides you: it is that same NavGraphBuilder scope that allows you to call navigation(...) {} to build a nested graph or composable to add a new destination to your graph.
All of the concepts are the same because the underlying NavGraph you construct ends up being the exact same set of objects at runtime, no matter how you actually build the graph.
I have a single activity app using only composables for the ui (one activity, no fragments). I use one viewmodel to keep data for the ui in two different screens (composables). I create the viewmodel in both screens as described in state documentation
#Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel())
Now I noticed that the data that was loaded or set in the first screen is reset in the second.
I also noticed that init{} is called every time viewModel() is called. Is this really the expected behavior?
According to the method's documentation it should return either an existing ViewModel or create a new one.
I also see that the view models are different objects. So viewModel() always creates a new one. But why?
Any ideas what I could be doing wrong? Or do I misunderstand the usage of the method?
Usually view model is shared for the whole composables scope, and init shouldn't be called more than once.
But if you're using compose navigation, it creates a new model store owner for each destination. If you need to share models between destination, you can do it like in two ways:
By passing it directly to viewModel call. In this case only the passed view model will be bind to parent store owner, and all other view models created inside will be bind(and so destroyed when route is removed from the stack) to current route.
By proving value for LocalViewModelStoreOwner, so all composables inside will be bind to the parent view model store owner, and so are not gonna be freed when route is removed from the stack.
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
val model = viewModel<Model>(viewModelStoreOwner = viewModelStoreOwner)
}
composable("second") {
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
val model = viewModel<Model>()
}
}
}
I'm trying to set up conditional navigation for my Fragments using the Navigation components and a BottomNavigationView.
Current setup (without conditions):
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
NavigationUI.setupWithNavController(bottom_navigation, navController)
General navigation is working fine, but I want to restrict user interaction with the bottom navigation based on conditions. If the condition is not met at the time of clicknig the menu item, this should only result in showing a Toast instead of navigating to the next fragment.
I already looked up this but the solution mentioned there involves navigating to the next fragment first and then check the conditions - but I want to avoid this.
Thank you very much.
Expose LiveData that will stream which #Ids are disabled.
class MainViewModel{
val disableNavigation = MutableLiveData<#Ids Int>()
fun invalidateNavigation() {
val canNavigate = ....
if(!canNavigate){
disableNavigation.value = R.id.bottom_nav_item_x
}
}
}
Then just observe this:
mainViewModel.observe(viewLifecycleOwner){
//disable the id here
//re-enable the rest of the items
}