compose-destinations icon indicating copy to clipboard operation
compose-destinations copied to clipboard

Start destination with mandatory arguments if it is a nested NavGraph

Open MaxMichel2 opened this issue 2 years ago β€’ 25 comments

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!

MaxMichel2 avatar Jul 18 '22 16:07 MaxMichel2

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!

raamcosta avatar Jul 20 '22 18:07 raamcosta

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.

ianhanniballake avatar Aug 05 '22 16:08 ianhanniballake

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 avatar Aug 05 '22 18:08 raamcosta

@raamcosta Hello, any updates on this?

ModAlpha avatar Aug 10 '22 09:08 ModAlpha

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.

raamcosta avatar Aug 10 '22 10:08 raamcosta

Hi! Is there any update on when this might be included?

Maikerupitazu avatar Oct 09 '22 07:10 Maikerupitazu

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 πŸ™‚

raamcosta avatar Oct 09 '22 08:10 raamcosta

Can i put activity intent params or SavedStateHandle to inner ViewModel over hilt for start screen?

JajaComp avatar Dec 30 '22 12:12 JajaComp

Hi @JajaComp ! Sorry but I'm not sure I follow πŸ€” Would you mind elaborating on the question?

raamcosta avatar Dec 30 '22 12:12 raamcosta

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.

JajaComp avatar Dec 30 '22 13:12 JajaComp

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 avatar Dec 30 '22 15:12 raamcosta

@raamcosta Can i put arguments to SavedStateHandle for get them from view model like "savedStateHandle.navArgs()" (Over navArgsDelegate)?

JajaComp avatar Dec 30 '22 15:12 JajaComp

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 πŸ™‚

raamcosta avatar Dec 30 '22 15:12 raamcosta

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 avatar Dec 30 '22 15:12 raamcosta

@raamcosta In clean Android compose navigation i can use default value for put them to default screen arguments

JajaComp avatar Dec 30 '22 15:12 JajaComp

@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 avatar Dec 30 '22 15:12 raamcosta

@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

JajaComp avatar Dec 30 '22 15:12 JajaComp

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!

raamcosta avatar Dec 30 '22 15:12 raamcosta

I agree with you. In fact, getting an intent from an Activity is a DI task. I will use solution with manualComposableCallsBuilder. Thx!

JajaComp avatar Dec 30 '22 15:12 JajaComp

support for that would also be greatly appreciated from my side πŸ™

chriswiesner avatar May 22 '23 11:05 chriswiesner

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 πŸ™‡

yurihondo avatar May 29 '23 02:05 yurihondo

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 πŸ™‚

raamcosta avatar May 29 '23 11:05 raamcosta

Hello, also interested in this feature :)

SammYWin avatar Jun 29 '23 18:06 SammYWin

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)
        }
        ....

Jayme-Rutkoski-SHG avatar Jul 20 '23 17:07 Jayme-Rutkoski-SHG

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
) 

vivekgupta4Git avatar Dec 12 '23 10:12 vivekgupta4Git