I want to have two versions of nav_graph in two build variant so and the first variant will use the main nav_graph and the second variant will add some more destinations to the main nav_graph so this makes the second graph duplicate of the main nav_graph but with more destinations which is hard to manage and keep update both at the same time so to solve this i go for dynamic navigation by creating a navigation graph by code which works fine but when i want to add a nested graph for second build variant destinations to the MainGraph it's not working
this file is in the main code base
MainGraph
object MainGraph{
var id_counter = 1
private val id = id_counter++
object Destinations {
val mainFragment = id_counter++
// ...
}
object Actions {
// ...
val to_settingsFragment = id_counter++
}
fun setGraph(context: Context, navController: NavController) {
navController.graph = navController.createGraph(id, Destinations.mainFragment) {
fragment<MainFragment>(Destinations.mainFragment) {
label = context.getString(R.string.app_name)
// ...
action(Actions.to_settingsFragment) {
destinationId = Destinations.settingsFragment
}
}
// ...
fragment<SettingsFragment>(Destinations.settingsFragment) {
label = context.getString(R.string.settings)
}
}
}
}
this file only in the second build variant
NestedGraph
object NestedGraph {
val id = MainGraph.id_counter++
object Destinations {
val nestedFirstFragment = MainGraph.id_counter++
val nestedSecondFragment = MainGraph.id_counter++
}
object Actions {
val to_nestedSecondFragment = MainGraph.id_counter++
}
fun addDestinations(navController: NavController) {
val navGraph = navController.createGraph(
this#NestedGraph.id,
Destinations.nestedFirstFragment
) {
fragment<NestedFirstFragment>(Destinations.nestedFirstFragment) {
action(Actions.to_nestedSecondFragment) {
destinationId = Destinations.nestedSecondFragment
}
}
fragment<NestedSecondFragment>(Destinations.nestedSecondFragment)
}
navController.graph.addAll(navGraph) // not working
}
}
After some debugging and try n fail i found that there is not wrong with NavGraph.addAll() method its working find it's just my mistake i was using the nested graph id instead of destination id when calling navigate and if anyone in future also get this kind of behaviour just make sure make the id variable private
Related
I have a composable function declared like this:
fun ScreenA(
nav: NavController,
type: SomeTypeObject,
) {
val vm = getViewModel<SomeTypeObjectViewModel>()
val state = rememberScaffoldState()
val scope = rememberCoroutineScope()
LaunchedEffect(LocalContext.current) {
when(type) {
SomeTypeObject.TYPE1 ->{
vm.updateState("1")
}
SomeTypeObject.TYPE2 -> {
//do something else
}
}
}
SomeTypeObjectViewModel contains state variable of my ScreenA like this:
var remeberVal = mutableStateOf<SomeTypeObject?>(null)
Now at some point in another composable function i use my navigationGraph to open another instance of ScreenA, so SomeTypeObjectViewModel gets recreated and remeberVal restes istelf but i want keep and reuse it when new instance of ScreenA is made.
Passing remeberVal as argument using the navigationGraph is not an option since you can only pass Strings, ints or parcelable objects which is not my case, considering that remeberVal has MutableState<SomeTypeObject?> type.
At this point my question is:
Is there a way to pass remeberVal to the new instance of ScreenA or to avoid SomeTypeObjectViewModel being reinstantiated after when i re-route to ScreenA using my navigaion graph?
Thank you!
Edit:
my getViewModel() is a Koin function to injevt the ViewModel, the internal code is:
org.koin.androidx.compose ViewModelComposeExtKt.class #Composable
public inline fun <reified T : ViewModel> getViewModel(
qualifier: Qualifier?,
owner: ViewModelStoreOwner,
scope: Scope,
noinline parameters: ParametersDefinition? /* = (() → ParametersHolder)? */
): T
The navigation graph is made in something like this way:
fun MyNGraph(nav: NavHostController) {
composable(
route = Routes.CaseType1.route + "/{someParameters}/",
arguments = listOf(
navArgument("someParameters") {},
),
) { backStackEntry ->
val someParameters = backStackEntry.arguments?.getString("someParameters")
someParameters?.let { someParameters ->
ScreenA(
type = SomeTypeObject.TYPE1, // Notice here, where i change type but use the same screen
)
}
}
}
composable(
route = Routes.CaseType2.route + "/{someParameters}/",
arguments = listOf(
navArgument("someParameters") {},
),
) { backStackEntry ->
val someParameters = backStackEntry.arguments?.getString("someParameters")
someParameters?.let { someParameters ->
ScreenA(
type = SomeTypeObject.TYPE2, // Notice here
)
}
}
}
}
You are using Koin for DI, so you can just add a dependency with a broader scope than your SomeTypeObjectViewModel that will hold the state you want to share between different screens/composables or between different VM instances. In that way your VMs have access to a shared state (a shared state holder is usually called a Repository).
class MySharedState {
// this could also be a MutableState instead of MutableStateFlow
// but then you are spreading the androidx.compose.runtime dependency
// to a shared state that should not need to know about Compose
val typeFlow = MutableStateFlow<SomeTypeObject?>(null)
}
class SomeTypeObjectViewModel(
val sharedState: MySharedState
): ViewModel() {
fun updateType(type: SomeTypeObject) {
sharedState.typeFlow.value = type
}
fun updateState(value: String) {
// your existing logic...
// call updateType(...) when you want to update the type
}
// rest of your ViewModel code
}
Where you are configuring your Koin modules add (if you are using Koin 3.2+)
module {
// a shared state scoped to the whole app lifecycle
singleOf(::MySharedState) // <-- add this
viewModelOf(::SomeTypeObjectViewModel) // <-- you probably already have this
}
If you are using Koin < 3.2
module {
// a shared state scoped to the whole app lifecycle
single { MySharedState() } // <-- add this
viewModel { SomeTypeObjectViewModel(get()) } // <-- you probably already have this but add one more get()
}
If you also want to access the state in your composables, you can use Flow.collectAsState()
fun ScreenA(
nav: NavController,
type: SomeTypeObject,
) {
val vm = getViewModel<SomeTypeObjectViewModel>()
val currentType by vm.sharedState.typeFlow.collectAsState()
// ...
}
by scoping your ViewModel to navigation routes or the navigation graph you can retrieve the same instance of your ViewModel
visit https://developer.android.com/jetpack/compose/libraries#hilt-navigation
#Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
navigation(startDestination = innerStartRoute, route = "Parent") {
// ...
composable("exampleWithRoute") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("Parent")
}
val parentViewModel = hiltViewModel<ParentViewModel>(parentEntry)
ExampleWithRouteScreen(parentViewModel)
}
}
}
}
From the docs, I see you can nest navigation graphs like so:
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
I am wondering, how would one pass an argument in the route, and make that available to all composables inside the nav graph?
Here's my current nav graph:
navigation(
// I'd like to grab this parameter
route = "dashboard?classId={classId}",
startDestination = Route.ScreenOne.route) {
composable(Route.ScreenOne.route) {
// And then pass the parameter here, or to any composable below
ScreenOne(classId)
}
composable(Route.ScreenTwo.route) {
ScreenTwo()
}
composable(Route.ScreenThree.route) {
ScreenThree()
}
}
I am basically trying to avoid setting the classId navigation argument individually on each composable route. I didn't see a way to pass a list of arguments to navigation() like you can in a composable().
It might be that what I am describing isn't possible, but looking forward to anyone's thoughts!
You can access the graph arguments from child composables:
navController.getBackStackEntry("dashboard?classId={classId}").arguments?.getString("classId")
From a quick test within a Hilt-based project, it looks like passing a property in as an argument to a navigation graph component results in the property being available in the savedStateHandle for any ViewModels that are made whilst that graph is in memory.
For example:
// 1. Define your routes.
sealed class Destination(val route: String) {
object TestGraph : Destination("TEST_GRAPH/{${Arguments.testParameter}}") {
object Arguments {
const val testParameter = "testParameter"
}
fun route(testParameter: String): String {
return "TEST_GRAPH/$testParameter"
}
object FirstScreen : Destination("FIRST_SCREEN")
}
}
// 2. Create a graph extension on NavGraphBuilder for the navigation graph.
private fun NavGraphBuilder.testGraph(navController: NavHostController) {
navigation(
startDestination = Destination.TestGraph.FirstScreen.route,
route = Destination.TestGraph.route,
arguments = listOf(
navArgument(Destination.TestGraph.Arguments.testParameter) { type = NavType.StringType }
)
) {
composable(route = Destination.TestGraph.FirstScreen.route) {
FirstScreen()
}
}
}
// 3. Use the line below to navigate to this new graph.
navController.navigateTo(Destination.TestGraph.route("xyz"))
// 4. Access the savedStateHandle via the VM to get the parameter.
#Composable
fun FirstScreen(
viewModel: FirstScreenViewModel = hiltViewModel(),
) {
//...
}
#HiltViewModel
class FirstScreenViewModel #Inject constructor(
savedStateHandle: SavedStateHandle,
): ViewModel() {
private val testParameter: String = checkNotNull(savedStateHandle[Destination.TestGraph.Arguments.testParameter])
//...
}
I assume this works as the argument is created and maintained at the navigation graph-level. So, providing the graph is in memory, the property is accessible via savedStateHandle. If you were to pop this graph off of the navigation stack, I would expect the value to not be accessible anymore. Hope that helps!
I am setting a navigation graph programmatically to set the start destination depending on some condition (for example, active session), but when I tested this with the "Don't keep activities" option enabled I faced the following bug.
When activity is just recreated and the app calls method NavController.setGraph, NavController forces restoring the Navigation back stack (from internal field mBackStackToRestore in onGraphCreated method) even if start destination is different than before so the user sees the wrong fragment.
Here is my MainActivity code:
class MainActivity : AppCompatActivity() {
lateinit var navController: NavController
lateinit var navHost: NavHostFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
log("fresh start = ${savedInstanceState == null}")
navHost = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment
navController = navHost.navController
createGraph(App.instance.getValue())
}
private fun createGraph(bool: Boolean) {
Toast.makeText(this, "Is session active: $bool", Toast.LENGTH_SHORT).show()
log("one: ${R.id.fragment_one}, two: ${R.id.fragment_two}")
val graph =
if (bool) {
log("fragment one")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_one
}
} else {
log("fragment two")
navController.navInflater.inflate(R.navigation.nav_graph).also {
it.startDestination = R.id.fragment_two
}
}
navController.setGraph(graph, null)
}
}
App code:
class App : Application() {
companion object {
lateinit var instance: App
}
private var someValue = true
override fun onCreate() {
super.onCreate()
instance = this
}
fun getValue(): Boolean {
val result = someValue
someValue = !someValue
return result
}
}
Fragment One and Two are just empty fragments.
How it looks like:
Repository with full code and more explanation available by link
My question: is it a Navigation library bug or I am doing something wrong? Maybe I am using a bad approach and there is a better one to achieve what I want?
As you tried in your repository, It comes from save/restoreInstanceState.
It means you set suit graph in onCreate via createGraph(App.instance.getValue()) and then fragmentManager in onRestoreInstanceState will override your configuration for NavHostFragment.
So you can set another another time the graph in onRestoreInstanceState. But it will not work because of this line and backstack is not empty. (I think this behavior may be a bug...)
Because of you're using a graph (R.navigation.nav_graph) for different situation and just change their startDestination, you can be sure after process death, used graph is your demand graph. So just override startDestination in onRestoreInstanceState.
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
if (codition) {
navController.graph.startDestination = R.id.fragment_one
} else {
navController.graph.startDestination = R.id.fragment_two
}
}
Looks like there is some wrong behaviour in the library and my approach wasn't 100% correct too. At least, there is the better one and it works well.
Because I am using the same graph and only changing the start destination, I can simply set that graph in onCreate of my activity and set some default start destination there. Then, in createGraph method, I can do the following:
// pop backStack while it is not empty
while (navController.currentBackStackEntry != null) {
navController.popBackStack()
}
// then just navigate to desired destination with additional arguments if needed
navController.navigate(destinationId, destinationBundle)
I created a class to handle different back stacks for each tab inside my app, hence am using different nav controllers with a "currentcontroller" field to get the current one :
private val navNewsController: NavController = obtainNavHostFragment(fragmentTag = "news", containerId = R.id.newsTabContainer).navController.apply {
graph = navInflater.inflate(R.navigation.navigation_graph_main).apply {
startDestination = startDestinations.getValue(R.id.tab_news)
}
addOnDestinationChangedListener { controller, destination, arguments ->
onDestinationChangedListener?.onDestinationChanged(controller, destination, arguments)
}
}
val navFormController: NavController = obtainNavHostFragment(fragmentTag = "form", containerId = R.id.formTabContainer).navController.apply {
graph = navInflater.inflate(R.navigation.navigation_graph_main).apply {
startDestination = startDestinations.getValue(R.id.tab_form)
}
addOnDestinationChangedListener { controller, destination, arguments ->
onDestinationChangedListener?.onDestinationChanged(controller, destination, arguments)
}
}
private fun obtainNavHostFragment(
fragmentTag: String,
containerId: Int
): NavHostFragment {
val existingFragment = mainActivity.supportFragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
existingFragment?.let { return it }
val navHostFragment = NavHostFragment.create(R.navigation.navigation_graph_main)
mainActivity.supportFragmentManager.beginTransaction()
.add(containerId, navHostFragment, fragmentTag)
.commitNowAllowingStateLoss()
return navHostFragment
}
And when I switch tabs I just change the "currentController":
fun switchTab(tabId: Int, goToRoot: Boolean = false) {
currentFragment()?.onPause()
currentTabId = tabId
when (tabId) {
R.id.tab_news -> {
currentController = navNewsController
invisibleTabContainerExcept(newsTabContainer)
}
R.id.tab_form -> {
currentController = navFormController
invisibleTabContainerExcept(formTabContainer)
}
....
So I have this FragmentA that opens from both news and form.
Whenever I open FragmentA from news and then FragmentA from form, FragmentA from news gets reloaded with the new arguments opened from form.
I tried using different actions inside the nav graph, I tried declaring the fragment twice with different ids and then different actions for the respective ids. I also tried making "newsAFragment" and "formAFragment" by just extending the original "AFragemnt" and still doesnt' work.
I also tried nav options:
NavOptions.Builder().setLaunchSingleTop(false).build()
How can I use multiple instances of the same fragment class inside a nav graph?
Turns out the problem is with the ViewModel not the fragment itself.. It was using the same instance of the view model. Instead i know use a unique key for each instance from the viewmodelstore
The docs of AndroidX Navigation currently mostly cover usage from xml.
I'd like to see an example of programmatic usage with Kotlin, with Fragments (because I'm not aware of another navigator at the moment).
Here's a simple example of how one can use AndroidX Navigation programmatically using Fragments with the KTX artifacts:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val container = frameLayout(id = R.id.content) // From Splitties Views DSL. Equivalent to FrameLayout().apply { id = R.id.content }
setContentView(container)
// Add the NavHostFragment if needed
if (savedInstanceState == null) supportFragmentManager.transaction(now = true) {
val fragment = NavHostFragment()
add(R.id.content, fragment)
setPrimaryNavigationFragment(fragment)
}
// Use the Kotlin extension from the -ktx dependencies
// to find the NavController for the given view ID.
val navController = findNavController(R.id.content)
// Create the graph using the Kotlin DSL, setting it on the NavController
navController.graph = navController.createGraph(startDestination = R.id.nav_dest_main) {
fragment<MainFragment>(R.id.nav_dest_main) {
label = TODO("Put an actual CharSequence")
}
fragment<SomeFragment>(R.id.nav_dest_some_fragment) {
label = TODO("Put an actual CharSequence")
}
}
}
In addition to the Louis example, take a look at the official nav-component guide - Build a graph programmatically using the Kotlin DSL.