compose-destinations
compose-destinations copied to clipboard
Start destination with mandatory arguments if it is a nested NavGraph
Hello again,
I was looking through the issues recently and found this one where you explained that a start destination cannot have a mandatory navigation argument. I 100% agree with your argument that being a start destination, mandatory arguments would prevent the app from launching.
However, in the case of a nested navigation graph, I feel that this argument becomes invalid. A start destination in a nested navigation graph should behave in some way as a destination of the RootNavGraph and therefore be allowed to have mandatory arguments.
Similarly, when navigating to a nested navigation graph using the actual NavGraph data class, users should be required to provide said mandatory arguments (potentially via a lambda or a vararg).
I will try to illustrate what I imagine:
Assume that we have the RootNavGraph
which contains a nested nav graph call NestedNavGraph
and another nested nav graph inside the previous one called NestedNestedNavGraph
(sorry about the naming).
// NestedNestedNavGraph start screen with mandatory argument
@NestedNestedNavGraph(
start = true
)
@Destination
@Composable
fun NestedNestedStartScreen(
navigator: DestinationsNavigator,
mandatoryName: String
) {
/* The name could be displayed on screen or stored in a view model for later use but is required for this NavGraph */
}
// NestedNavGraph which navigated to NestedNestedNavGraph giving it its mandatory argument
@NestedNavGraph(
start = true
)
@Destination
@Composable
fun NestedStartScreen(
navigator: DestinationsNavigator
) {
/* Some things happen and maybe user input gives us a String which we store in the variable 'name' */
navigator.navigate(
direction = NavGraphs.nestednested,
name // Would be provided via a vararg parameter
)
}
Another alternative could be to require the implementation of a navArgsDelegate containing said mandatory arguments to be provided to the screen.
I'm not 100% convinced by my implementation ideas but I believe you'll more than likely understand the general direction I have in mind. I'm totally available if you need more information on the issue!
Hi @MaxMichel2 !
I investigated this a bit. Looks like what made me think even nested nav graph with arguments were not possible was this:
https://github.com/google/accompanist/issues/892
Until recently, accompanist navigation
method used for registering a nested nav graph, did not receive arguments/deep links arguments.
However, that is resolved, so I definitely need to take a look at this π
Thanks for letting me know. I hope to start working on it soon!
FWIW, https://github.com/google/accompanist/issues/892 was never, ever blocking this work, as arguments, deep links, etc. can be directly added to the lambda following the navigation
element via the Navigation Kotlin DSL argument
and Navigation Kotlin DSL deepLink
methods as explained in the comment on that issue.
Yeah @ianhanniballake I noticed your comment in the issue. Thanks a lot btw, this was definitely my bad that I didnβt notice these APIs π
And itβs good that I donβt need the issue fixed to add this support. This way I can do it for older versions using older accompanist version.
@raamcosta Hello, any updates on this?
Iβm working on this at the moment. It will make quite a few changes so it will take a while and probably will have to bump the major or minor version because some APIs will inevitably change a bit.
Hi! Is there any update on when this might be included?
Admittedly the work here as been slow since I've had very little free time these days. It's hard to give an ETA, but let's say it should definitely be before end of the year π€
Hopefully, this is not a stopper for anyone, since you can use non-mandatory arguments (give like null default) and assert on the receiving end. It's not the best, I know, but should get things going until you will be able to improve it π
Can i put activity intent params or SavedStateHandle to inner ViewModel over hilt for start screen?
Hi @JajaComp ! Sorry but I'm not sure I follow π€ Would you mind elaborating on the question?
Hi @raamcosta ! I use some activity for get shared elements over intent-filter. Activity got this data over intent. How i can push this intent data from activity to compose view model or navArgsDelegate? This screen is started screen in navigation.
If this screen is your starting screen, then navigation arguments make no sense since we don't "navigate" to our first screen. So in that case you want to pass this data as something you pass from your "NavHost level", for this you can do it in two ways, most easy for you probably is to manually call your screen Composable and pass whatever else you need (or get a hold of your ViewModel and set the data).
Read more here: https://composedestinations.rafaelcosta.xyz/destination-arguments/navhost-level-parameters#manually-call-your-screen-composable
@raamcosta Can i put arguments to SavedStateHandle for get them from view model like "savedStateHandle.navArgs()" (Over navArgsDelegate)?
No because navArgsDelegate
is about navigation arguments and navigation arguments is what you send from one screen to the next. Since this is your first screen, there are no navigation arguments π
You can instead have a setData()
method in your ViewModel, which you call just before calling your actual screen Composable, like this:
DestinationsNavHost(
navGraph = NavGraphs.root
//...
) {
composable(SomeScreenDestination) {
val viewModel = [YOUR VIEWMODEL GETTER METHOD]<SomeViewModel>()
viewModel.setData([DATA YOU GET IN YOUR ACTIVITY VIA INTENT])
SomeScreen()
}
}
replacing all the [CAPS]
and the "SomeScreen" with your correct stuff π of course!
@raamcosta In clean Android compose navigation i can use default value for put them to default screen arguments
@JajaComp in official compose navigation, your initial screen also cannot have navigation arguments. Not sure what you mean, can you show me some code?
@raamcosta Activity:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "start/{data}",
modifier = Modifier.fillMaxSize(),
) {
composable(
route = "start/{data}",
arguments = listOf(
navArgument("data") {
defaultValue = intent.extras
type = NavType.ParcelableType(Bundle::class.java)
},
),
) {
val viewModel: ShareReceiverBookViewModel = hiltViewModel()
println(viewModel.savedStateHandle)
}
}
}
}
ViewModel:
@HiltViewModel
internal class ShareReceiverBookViewModel @Inject constructor(
val savedStateHandle: SavedStateHandle,
) : ViewModel() {
init {
println(savedStateHandle)
}
}
In view model i can got activity intent extras over savedStateHandle
I see, thank you for the code π
However, I would say that is subverting the goal of the API. This is not how the API is intended to be used. In fact, if you do this, you could have other screens from your app navigating to the initial screen and passing something else that would "override" the default value you get from the intent.
You are basically creating a contract for your initial screen that says "I can be navigated to and I receive arguments of type Bundle, if someone navigates to me but doesn't provide this argument, then I get it from activity intent". So you're opening a door for a navigation action to take some other Bundle, which is not safe and feels very weird. The reason this doesn't work on Compose Destinations is that there was a conscious decision to not have arguments defined at runtime (this is where we get the compile time safety). So there is no way of saying "this screen has these arguments and this is the default value" at runtime. Again, this was a conscious decision to get all the benefit of compile-time type safety and also, we don't really loose anything because these usages are weird and open weird doors π
Hope I made myself clear! Let me know if this makes sense and if I can help any further!
I agree with you. In fact, getting an intent from an Activity is a DI task. I will use solution with manualComposableCallsBuilder. Thx!
support for that would also be greatly appreciated from my side π
I have been using Compose Destinations, and it is extremely useful in my app. It would be fantastic if this issue is resolved and we can pass default arguments to the start route of Nested Graphs. I am eagerly looking forward to the progress on this issue π
Thanks for all the feedback guys! I will definitely add this for version 2.0
Now that navigation supports animations without accompanist, this will requires some changes from this lib and I will also do the required changes to add this feature.
I canβt give exact ETA but I am working on it every chance I get π
Hello, also interested in this feature :)
Just FYI, not sure if this might help someone but a temporary workaround if you use NavigationBar would be to have your default tab "navigate" upon loading the NavigationBar (using LaunchedEffect), that way you can pass in whatever arguments you might need. This worked for us, hopefully it helps someone else.
NavigationBar(
modifier = Modifier.height(height.dp),
containerColor = backgroundColor,
) {
LaunchedEffect(Unit) {
items[0].navigate(navigator)
}
....
Till this issue get resolved I created workaround by creating viewModel with dependency. The required argument is passed on to the viewModel by the activity. My requirement was the id of the category of type string.
private lateinit var categoryViewModel : CategoryViewModel
private var categoryId = ""
//inside onCreate()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
communityViewModel.currentFeedCategoryId.collectLatest {
categoryId = it
Log.d("MyLog","Collecting id = $categoryId")
}
}
}
where the communityViewModel has following elements :
private var _currentFeedCategoryId = MutableStateFlow("")
val currentFeedCategoryId = _currentFeedCategoryId.asStateFlow()
fun setCategoryId(id : String){
_currentFeedCategoryId.value = id
}
The method setCategory is called from the composable which wants to navigate to the composable which require this id.
onCategoryClick = { category ->
viewModel.setCategoryId(category)
//navigate to category feed screen
navigator.navigate(FeedByCategoryScreenDestination())
}
Now inside a dependencyContainerBuilder
dependency(NavGraphs.category){
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(NavGraphs.category.route)
}
//create a factory
val factory = CategoryViewModelFactory(categoryId,useCases)
categoryViewModel =ViewModelProvider(parentEntry,factory)[CategoryViewModel::class.java]
categoryViewModel
}
where my category navGraph is nested inside root graph
@RootNavGraph(start = false)
@NavGraph
annotation class CategoryNavGraph(
val start : Boolean =false
)
Thus, my nested category screen reduced to this
@CategoryNavGraph(start = true)
@Destination
@Composable
fun FeedByCategoryScreen(
navigator: DestinationsNavigator,
categoryViewModel: CategoryViewModel
)