I'm not really sure what I'm doing wrong here. Simple setup, single activity with fragments controlled by a bottom bar inside, so far so good. The start fragment has a button inside which should navigate to another fragment.
Here's the onclicklistener for the button inside my fragment class:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
add_step_fab.setOnClickListener {
Navigation.findNavController(view).navigate(R.id.fragmentAtoB)
}
}
Here's my action in the navigation.xml:
<fragment android:id="#+id/navigation_main" android:name="com.test.test.fragments.AFragment"
android:label="Fragment A" tools:layout="#layout/fragment_a">
<action android:id="#+id/fragmentAtoB"
app:destination="#id/fragmentB" app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim" app:popEnterAnim="#anim/nav_default_pop_enter_anim"
app:popExitAnim="#anim/nav_default_pop_exit_anim"/>
</fragment>
The navigation works, but I get the standard
java.lang.IllegalArgumentException: navigation destination com.test.test:id/fragmentAtoB is unknown to this NavController
error when I click on the button after rotating my screen, split screening the app, or sometimes seemingly randomly. Looks like it has something to do with the configuration change?
EDIT:
I also tried using
val clickListener: View.OnClickListener = Navigation.createNavigateOnClickListener(R.id.fragmentAtoB)
add_step_fab.setOnClickListener(clickListener)
with the same issue as above.
Checking if i'm actually in the same fragment, as some suggested for users who are having the same exception (but for unrelated reasons), such as:
if (controller.currentDestination?.id == R.id.navigation_main) {
controller.navigate(R.id.fragmentAtoB)
}
also didn't help.
I also experienced this recently after months of an app working correctly. I originally had added navigation to an app that was utilizing a "MainActivity" class and then had other activities that would be launched from a menu (as opposed to having the Navigation component navigate between fragments off of a single Activity).
So the app is organized such that the MainActivity is the "hub", and choices from menu options cause other activities to be launched, and when the user returns from them they are back at the MainActivity - so a pure "hub (MainActivity) and spoke (5 other activities)" model.
Within MainActivity.onCreate() is called a setupNavigation() method which originally looked like:
private void setupNavigation() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
drawerLayout = findViewById(R.id.drawer_layout);
// This class implements the listener interface for the NavigationView, handler is below.
NavigationView navigationView = findViewById(R.id.nav_view);
navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout);
NavigationUI.setupWithNavController(navigationView, navController);
// MainActivity implements the listener for navigation events
navigationView.setNavigationItemSelectedListener(this);
}
As I mentioned, this was working just fine even with configuration changes until recently. Even just changing the configuration from portrait to landscape on the MainActivity would cause an exception:
ComponentInfo{com.reddragon.intouch/com.reddragon.intouch.ui.MainActivity}: java.lang.IllegalStateException: You must call setGraph() before calling getGraph()
I found I had to add this line:
navController.setGraph(R.navigation.nav_host);
Right after the Naviation.findNavController() call, and then it handled configuration changes just fine.
After a configuration change, the fragment or activity goes out of scope temporarily and then executes an onResume() override. If you have anything which is initialized in onCreate(), or onViewCreated(), then those variables may have gone out of scope and are now null values. So, I usually have a method called RestoreAll() which is responsible for instantiating all of the objects used by the code, and I call this method from onResume() so that every time there is a configuration change, all the local objects are re-instantiated.
There is a problem with nav controller backstack after changed config. Try this:
https://stackoverflow.com/a/59987336/5433148
Related
I started noticing something in my app the other day and its wildly inconsistent. Sometimes it happens and sometimes it doesn't.
I am using the Navigation Component to handle navigation in the app and I started noticing that sometimes, when popping the backstack via the action bar back button or the device back button, it returns to a fragment that is no longer the starting destination (or at least shouldn't be).
In my case the app starts in MainFragment and once authenticated moves to DashboardFragment. This is a pretty common scenario.
Navigation in the app is pretty flat. most of the time its only 1 level deep so nearly all views are accessible from the dashboard.
The app starts at a login view as many do and then to a dashboard where the session will remain as the "start destination". To accomplish this, its done in the nav_graph using popUpTo and popUpToInclusive.
<fragment
android:id="#+id/mainFragment"
android:name="com.example.view.fragments.MainFragment"
android:label="Welcome">
<action
android:id="#+id/action_mainFragment_to_dashboardFragment"
app:destination="#id/dashboardFragment"
app:popUpTo="#id/mainFragment"
app:popUpToInclusive="true"/>
</fragment>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.example.view.fragments.dashboard.DashboardFragment"
android:label="#string/dashboard_header" >
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment" />
</fragment>
When the user successfully authenticates and its time to go to the dashboard, I use NavController.navigate() to send them there.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment()
)
// This should have the same result and it does appear to be affected by the same issue
// findNavController().navigate(R.id.action_mainFragment_to_dashboardFragment)
I have an action bar with a back arrow and a navigation drawer. In the main activity I need to define the AppBarConfiguration and override onSupportNavigateUp()
lateinit var appBarConfiguration: AppBarConfiguration
...
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate()")
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// There is 2 different drawer menu's respectfully.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.mainFragment,
R.id.dashboardFragment
), binding.drawerLayout
)
setSupportActionBar(binding.toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
...
override fun onSupportNavigateUp(): Boolean {
Timber.d("-> onSupportNavigateUp()")
val breadcrumb = navController
.backStack
.map { it.destination }
.filterNot { it is NavGraph }
.joinToString(" > ") { it.displayName.split('/')[1] }
Timber.d("Backstack: $breadcrumb")
Timber.d("Previous backstack entry: ${navController.previousBackStackEntry?.destination?.displayName}")
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
...
The logs look like this when we step back and it is working correctly
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: dashboardFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/dashboardFragment
D/DashboardFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/DashboardFragment: -> onViewCreated()
I also noticed when using the hamburger in the action bar that it also calls onSupportNavigateUp()
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: pendingAction: false
D/MainActivity: Backstack: dashboardFragment
D/MainActivity: Previous backstack entry: null
When I use the drawer to navigate to a destination I do see this in the logs and im not sure where/why this is returned or if it has any importance
I/NavController: Ignoring popBackStack to destination com.example:id/mainFragment as it was not found on the current back stack
Now, when its NOT working correctly, this is what the logs look like
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: mainFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/mainFragment
D/MainFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/MainFragment: -> onViewCreated()
This really feels like the popUpTo and popUpToInclusive properties are not being applied (sometimes) when performing the navigation from main fragment to dashboard fragment. It's also suspicious that even though the dashboard fragment is not set as the new starting destination but also its missing from the backstack. Assuming the properties were NOT applied I would expected to see the breadcrumb Backstack: mainFragment > dashboardFragment > testingFragment
Any help would be greatly appreciated!
While there may certainly be a better procedure to follow (as outlined in the Principles of Navigation in the previous comments) the overhead of the change introduces a plethora of new errors and the scope is too large at this time.
Its still unknown why popUpTo and popUpToInclusive are not reliable via XML when navigating (even using NavDirections) however, thus far, passing the NavOptions while navigating seems to resolve the issue.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment(),
NavOptions.Builder().setPopUpTo(R.id.mainFragment, true).build()
)
Thus far the issue has yet to arise again.
As it says in Ian's first link:
The back stack always has the start destination of the app at the bottom of the stack.
Your start destination is the "home" one in a navigation graph, the one identified with the little house icon. Set with the startDestination attribute in the XML. When the Navigation library creates a backstack, it always has that destination at the bottom. And it will always be there, even if you try to avoid it with the poUpTo attributes.
That's why if there's a fragment you consider your "home" one, like your dashboard, that needs to explicitly be your startDestination. It's the last thing a user will see if they back out of your app. If you have something like a login or welcome screen as the start destination, they'll back out to that.
Which is why you need to set the "home" fragment as the start destination, and then handle any extra navigation to a login or welcome screen from there. It's just how the Navigation stuff is designed to work. If you try and work around it (I did!) you'll run into other problems, and a lot of the nice features like automatic backstack recreation might not work properly
I'm relatively new with Kotlin and I'm trying to extend the navigation drawer example by adding a button into my fragment that can open another fragment directly without having to select it from the navigation drawer.
Here is my button listener in my fragment's onCreateView function:
addButton.setOnClickListener()
{
Toast.makeText(this.context,"ButtonPressed",Toast.LENGTH_SHORT).show()
this.activity.supportFragmentManager.beginTransaction().replace(R.id.mobile_navigation, R.layout.fragment2).commit()
}
I'm not sure I completely understand how to approach this. The button works fine and calls the toast, but I can't get it to change the fragment.
Any help would be appreciated.
the template has a mobile_navigation.xml file with a "navigation" element.
The Navigation Architecture components is used by default in recent android studio versions, so the navController is the responsible for fragment transaction instead of doing that manually by the supportFragmentManager
addButton.setOnClickListener() {
Toast.makeText(this.context,"ButtonPressed",Toast.LENGTH_SHORT).show()
val navHostFragment = requireActivity().supportFragmentManager
.primaryNavigationFragment as NavHostFragment
navHostFragment.navController.navigate(R.layout.fragment2)
}
Also, make sure that R.layout.fragment2 is the fragment id of the destination fragment in the R.id.mobile_navigation.xml
So I have this app with one activity (MainActivity) and I'm using the navigation architecture component. So it has DrawerLayout which is wrapping the fragment view.
These are two methods in the Activity class which are responsible for Locking and Unlocking of the drawerLayout.
...
fun lockNavDrawer() {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
}
fun unLockNavDrawer() {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
}
...
There are some fragments which need to have the navigation drawer. Everything is working fine except those fragments which not suppose to have the Drawer.
So, in my base fragment, and I'm implementing an interface (HasToolbar) which have a variable called hasNavigationDrawer and in the onDestroyView of the base fragment, I'm doing this. The locking of the Drawer is fine.
if (this is HasToolbar) {
if (hasNavigationDrawer) {
activity.lockNavDrawer()
}
unregisterListeners()
}
But when I do this in the onViewCreated of the base fragment
if (this is HasToolbar) {
if (hasNavigationDrawer) {
activity.unlockNavDrawer()
}
}
It does not unlock the drawer hence it does not open the drawer inside the fragment which suppose to have the drawer.
If I do this inside a fragment it works it stays unlocked.
lockNavDrawer()
unLockNavDrawer()
If you look at the ordering of the callbacks, you'll see that the onViewCreated() of the new Fragment is called before the onDestroyView() of the old Fragment. This makes particular sense when it comes to animations since to smoothly animate between two Fragments, both the new View and the old View must coexist and only after the animation ends does the old Fragment get destroyed. Fragments, in order to be consistent, use the same ordering whether you have an animation or not.
Ideally, your Fragments shouldn't be reaching up to the Activity at all to affect the Activity's behavior. As per the Listen for Navigation Events documentation, you should instead use an OnDestinationChangedListener to update your Activity's UI. The listener only receives information about the destination and its arguments however, so to use this approach you'd want to add an argument to your navigation graph:
<!-- this destination leaves the drawer in its default unlocked state -->
<fragment
android:id="#+id/fragment_with_drawer"
android:name=".MainFragment" />
<!-- the drawer is locked when you're at this destination -->
<fragment
android:id="#+id/fragment_with_locked_drawer"
android:name=".SettingsFragment">
<argument
android:name="lock_drawer"
android:defaultValue="true"/>
</fragment>
Then your OnDestinationChangedListener could look like
navController.addOnDestinationChangedListener { _, _, arguments ->
if(arguments?.getBoolean("lock_drawer", false) == true) {
lockNavDrawer()
} else {
unlockNavDrawer()
}
}
I'm using the Navigation Architecture Component and I have a setup similar to this one for popping the stack when navigating to a particular fragment:
<action
android:id="#+id/navigate_to_main_screen"
app:destination="#id/fragment_main_screen"
app:popUpTo="#+id/navigation_main"
app:popUpToInclusive="true"/>
This works almost as expected. Both the system back button and the up icon in the app bar don't navigate to the previous fragment. The system back button exits the app.
However, the up button in the app bar is still there, clicking it doesn't do anything as expected. What am I doing wrong? Why is this still here?
In the main activity I already have
AppBarConfiguration config =
new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupActionBarWithNavController(this, navController, config);
and
#Override
public boolean onSupportNavigateUp() {
return navController.navigateUp() || super.onSupportNavigateUp();
}
As per the documentation.
The library version I'm using:
implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha09'
implementation 'android.arch.navigation:navigation-ui:1.0.0-alpha09'
If you want to customize which destinations are considered top-level destinations, you can instead pass a set of destination IDs to the constructor, as shown below.
To solve your problem, replace
AppBarConfiguration config =
new AppBarConfiguration.Builder(navController.getGraph()).build();
With
AppBarConfiguration config =
new AppBarConfiguration.Builder(R.id.navigation_main, R.id.fragment_main_screen).build();
More details here: AppBarConfiguration
Navigating in onCreate method like so:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.magic_mile_host)
setSupportActionBar(toolbar_start_test)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back_black_24dp)
navController = findNavController(R.id.nav_host_magic_mile)
navigateToMyTests()
}
Here is my navigateToMyTests() implementation
navController.navigate(R.id.myTestsFragment)
The problem only appears when i invoke this function immediately.
The problem is when I'm on fragment which i came from myTestsFragment. After rotating screen the current fragment is not restored but myTestsFragment is restored always.
The reason why I did this way is because i want to ommit my startDestination in nav graph in certain situation.
Could you explain me why it's happening and maybe help me to come up with other solution to this problem?
In your case, which is about setting your start destination it's better to change it when it's needed using this line of code: navController.getGraph().setStartDestination(int id);
Another point you should pay attention is that calling your navigation methods inside the onCreate() in your Activity is risky, as the navHost so the FragmentManager might not be ready yet. Make sure your start destination is attached, then start your navigation process.