I am trying to write a test for a menu item. The idea is to click on the menu item and verify that the NavController is set to the correct destination:
public class NavigationTest {
#Test
public void clickOnSearchMenuNavigatesToFilter() {
TestNavHostController navController = new TestNavHostController(ApplicationProvider.getApplicationContext());
ActivityScenario<MainActivity> listScenario = ActivityScenario.launch(MainActivity.class);
listScenario.onActivity(activity -> {
navController.setGraph(R.navigation.nav_graph);
Navigation.setViewNavController(activity.requireViewById(R.id.nav_host_fragment), navController);
});
Espresso.onView(ViewMatchers.withId(R.id.filter_menu)).perform(ViewActions.click());
assertThat(Objects.requireNonNull(navController.getCurrentDestination()).getId()).isEqualTo(R.id.filter_cards);
}
}
This test fails with
expected: 2131296468
but was : 2131296378
2131296378 is the id for the initial fragment in the NavController. What is wrong with my test? My initial thinking is that I am not injecting the TestNavHostController correctly, but debugging verified that the activity sees it as I expect:
public class MainActivity extends AppCompatActivity {
// ...
#Override
public boolean onOptionsItemSelected(MenuItem item) {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
return NavigationUI.onNavDestinationSelected(item, navController)
|| super.onOptionsItemSelected(item);
}
}
When I set a breakpoint here, I confirm that navController is an instance of TestNavHostController.
For completeness, here's my nav_graph.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/card_list">
<fragment
android:id="#+id/card_list"
android:name="bbct.android.common.fragment.BaseballCardList"
android:label="#string/app_name"
tools:layout="#layout/card_list">
<action
android:id="#+id/action_details"
app:destination="#id/card_details" />
<action
android:id="#+id/action_filter"
app:destination="#id/filter_cards" />
<argument
android:name="filterParams"
app:argType="android.os.Bundle"
app:nullable="true" />
</fragment>
<fragment
android:id="#+id/about"
android:name="bbct.android.common.fragment.About"
android:label="#string/about_title"
tools:layout="#layout/about" />
<fragment
android:id="#+id/card_details"
android:name="bbct.android.common.fragment.BaseballCardDetails"
android:label="#string/card_details_title"
tools:layout="#layout/card_details">
<argument
android:name="id"
app:argType="long" />
</fragment>
<fragment
android:id="#+id/filter_cards"
android:name="bbct.android.common.fragment.FilterCards"
android:label="#string/filter_cards_title"
tools:layout="#layout/filter_cards">
<action
android:id="#+id/action_list"
app:destination="#id/card_list" />
</fragment>
<action
android:id="#+id/action_about"
app:destination="#id/about" />
</navigation>
Any ideas what I am missing or anything I can try to troubleshoot this test?
Addendum:
Note that my test here uses an ActivityScenario instead of a FragmentScenario as in the example from the documentation. I realize this might be an XY problem. I use a ActivityScenario in this test because I need to click on a MenuItem in the toolbar which is hosted in the Activity. In my previous attempts using a FragmentScenario, the toolbar wasn't rendered. I'm open to suggestions that use a different approach to the test I'm trying to write.
In order to tie navigation destinations to menu items, the IDs of both need to match as per documentation:
If the id of the MenuItem matches the id of the destination, the NavController can then navigate to that destination.
This is not exclusive to ActionBar menu, but also to other types of menus like Navigation Drawer & BottomNavigationView menus.
Without that in place, the navigation won't occur, and the test will fail because the current destination stays unchanged.
To fix this, both R.id.filter_menu & R.id.filter_cards need to be matched in menu & navGraph XML files, and also in the test, assuming both are filter_cards, then R.id.filter_menu needs to be changed in the menu file and in the test to be R.id.filter_cards:
Espresso.onView(ViewMatchers.withId(R.id.filter_cards)).perform(ViewActions.click());
Related
I'm working on an Android app (java) which currently only has a single activity (MainActivity) which loads four fragments via Bottom navigation. I'm using the Navigation component, this is the nav_graph:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.myapp.app.HomeFragment"
android:label="Home"
tools:layout="#layout/fragment_home">
// .. actions to sub fragments ..
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name="com.myapp.app.SecondFragment"
android:label="Second"
tools:layout="#layout/fragment_second" />
// .. actions to sub fragments ..
<fragment
android:id="#+id/thirdFragment"
android:name="com.myapp.app.ThirdFragment"
android:label="Third"
tools:layout="#layout/fragment_third">
// .. actions to sub fragments ..
</fragment>
<fragment
android:id="#+id/fourthFragment"
android:name="com.myapp.app.FourthFragment"
android:label="Fourth"
tools:layout="#layout/fragment_fourth" >
// .. actions to sub fragments ..
</fragment>
// .. More subfragments linked from the first four ..
</navigation>
What I'd like is to have a separate flow where I have an Onboarding Screen, which can link to Sign In or Sign Up. After Signing In or going through the process of Signing Up the user should return to the Home fragment with bottom navigation. The problem is, this whole second flow should not show either toolbar or bottom navigation, they should be full screen and shouldn't allow access to the bottom navigation layout.
How can I get the bottom navigation to just affect those fragments in the main app but not the screens in the sign in/sign up process?
You can use "addOnDestinationChangedListener" method of NavController in oncreate() of your MainActivity. Here is the sample code is written in kotlin.
navController.addOnDestinationChangedListener { controller, destination, arguments ->
when (destination.id) {
R.id.splashFragment, R.id.welcomeFragment, R.id.signInFragment, R.id.signUpFragment, R.id.forgotPasswordFragment,
R.id.forgotPasswordStepTwoFragment -> {
mToolbar.visibility = View.GONE
navViewBottom.visibility = View.GONE
}
R.id.homeFragment -> {
mToolbar.visibility = View.VISIBLE
navViewBottom.visibility = View.VISIBLE
}
else -> {
mToolbar.visibility = View.VISIBLE
navViewBottom.visibility = View.VISIBLE
}
}
You have to declare NavController in your MainActivity class for that.
private lateinit var navController: NavController
navController = findNavController(R.id.nav_host_fragment)
Here "nav_host_fragment" is fragment container in your xml file.
Hope it works...
<fragment
android:id="#+id/logInFragment"
android:name="com.android.bottomnavmultipletablayout.login.LogInFragment"
android:label="fragment_log_in"
tools:layout="#layout/fragment_log_in" >
<action
android:id="#+id/action_logInFragment_to_instagramFragment"
app:destination="#id/instagramFragment" />
</fragment>
<fragment
android:id="#+id/instagramFragment"
android:name="com.android.bottomnavmultipletablayout.main.InstagramFragment"
android:label="fragment_instagram"
tools:layout="#layout/fragment_instagram" />
<fragment
android:id="#+id/twitterFragment"
android:name="com.android.bottomnavmultipletablayout.main.TwitterFragment"
android:label="fragment_twitter"
tools:layout="#layout/fragment_twitter" />
<fragment
android:id="#+id/facebookFragment2"
android:name="com.android.bottomnavmultipletablayout.main.FacebookFragment"
android:label="fragment_facebook"
tools:layout="#layout/fragment_facebook" />
======================================================================
<item
android:id="#+id/instagramFragment"
android:title="instagram" />
<item
android:id="#+id/facebookFragment2"
android:title="facebook" />
<item
android:id="#+id/twitterFragment"
android:title="twitter" />
put your id fragment you want in navigation component in menu.
In my app I'm using navigation component
I created the navigation graph and actions and all seems working fine.
My only issue is the toolbar of the app. Its title is not getting updated
based on each destination label. However it only displays the app name.
Navigation
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="#+id/navigation"
app:startDestination="#id/loginFragment">
<fragment
android:id="#+id/loginFragment"
android:name="com.udacity.shoestore.login.LoginFragment"
android:label="LoginFragment"
tools:layout="#layout/fragment_login">
<action
android:id="#+id/action_loginFragment_to_welcomeFragment"
app:destination="#id/welcomeFragment"
app:enterAnim="#anim/fade_in"
app:exitAnim="#android:anim/fade_out"
app:popEnterAnim="#anim/fade_in" />
</fragment>
<fragment
android:id="#+id/welcomeFragment"
android:name="com.udacity.shoestore.welcome.WelcomeFragment"
android:label="WelcomeFragment"
tools:layout="#layout/fragment_welcome">
<action
android:id="#+id/action_welcomeFragment_to_instructionFragment"
app:destination="#id/instructionFragment"
app:enterAnim="#anim/fade_in"
app:exitAnim="#android:anim/fade_out"
app:popEnterAnim="#android:anim/fade_in" />
</fragment>
Application tag in Manifest
<application
android:name=".ShoeApplication"
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
There are few steps to set this up I'll break it down as easily as I can and provide notes along the way
Step 1 Create a Toolbar in the MainActivity XML.
<androidx.appcompat.widget.Toolbar
android:id="#+id/my_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="#style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light"
/>
You can find additional help in setting up Actionbar in the dev docs
The rest of the steps will take play in the MainActivity onCreate()
Step 2 set Toolbar
setSupporActionBar(findViewById(R.id.my_toolbar))
Step 3 create a reference for the navHostFragment
Because the view isn't loaded the fragment view cannot be found. For the navController to be properly set, the fragment view needs to be created and onViewCreated needs to be dispatched, which does not happen until the activity created state.
val navHostFragment: Fragment? = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
Step 4 Post the call to the findNavController method on a handler and execute all actions needed.
val navController = navHostFragment!!.findNavController()
Step 5 Combine the action bar with the NavController.
The start destination of your navigation graph is considered the only top-level destination. on all other destinations, the Action bar will show the Up button
NavigationUI.setupActionBarWithNavController(this, navController)
The MainActivity onCreate() should loo like this.
setSupportActionBar(findViewById(R.id.my_toolbar))
val navHostFragment: Fragment? = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
val navController = navHostFragment!!.findNavController()
NavigationUI.setupActionBarWithNavController(this, navController)
Step 6 handle up button
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.nav_host_fragment).navigateUp()
}
You can additional details here on the android dev docs
I have an app similar to Android Studios Navigation Drawer Activity:
My activity uses Android Architecture Navigation Components & a navigation drawer to navigate between different fragments.
As the navigation drawer is pretty custom i can't use the usual navigation-view, but use a custom fragment hosting a LinearLayout.
Each item in that LinearLayout has an onClickListener which boils down to
navController.navigate(R.id.myCorrespondingFragment)
So far, so everything works fine.
The problem begins when navigating back:
Let's imagine i navigate from "Home"-Fragment A -> B -> C and then go back.
Android Studios example behaves correctly: C -> A -> close
My implementation doesn't: it just pops the backstack C -> B -> A -> close
How do i fix that?
Minified Main-Layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout android:id="#+id/drawerLayout" >
<LinearLayout android:orientation="vertical" >
<androidx.appcompat.widget.Toolbar android:id="#+id/toolbar" />
<fragment
android:id="#+id/container"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"
/>
</LinearLayout>
<fragment
android:name="com.company.drawer.DrawerFragment"
android:layout_gravity="start"
/>
</androidx.drawerlayout.widget.DrawerLayout>
Navigation graph:
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/navigation_graph"
app:startDestination="#id/doorFragment"
>
<fragment
android:id="#+id/doorFragment"
android:name="com.company.door.DoorFragment"
android:label="#string/shared_empty"
/>
<fragment
android:id="#+id/historyFragment"
android:name="com.company.history.HistoryFragment"
android:label="#string/history"
/>
<fragment
android:id="#+id/settingsFragment"
android:name="com.company.settings.SettingsFragment"
android:label="#string/settings"
/>
</navigation>
Everything navigation related from my Main Activity's onCreate:
setSupportActionBar(toolbar)
val navController = (supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment).navController
toolbar.setupWithNavController(navController, AppBarConfiguration(
listOf(R.id.doorFragment, R.id.historyFragment, R.id.settingsFragment)
, drawerLayout
))
You can try like this
<fragment
android:id="#+id/fragmentA"
android:name="com.package.FragmentA"
android:label="Fragment A">
<action
android:id="#+id/action_fragmentA_to_fragmentB"
app:destination="#id/fragmentB"
app:popUpTo="#id/fragmentB"
app:popUpToInclusive="true / false" />
</fragment>
You can use global actions, so that you don't have to add an action for every fragment to fragment navigation but only for every fragment destination. This would look like this in XML:
<action android:id="#+id/action_global_doorFragment"
app:destination="#id/doorFragment"
app:popUpTo="#id/#id/doorFragment"
app:restoreState="true"
app:popUpToSaveState="true" />
<action android:id="#+id/action_global_historyFragment"
app:destination="#id/historyFragment"
app:popUpTo="#id/#id/doorFragment"
app:restoreState="true"
app:popUpToSaveState="true" />
<action android:id="#+id/action_global_settingsFragment"
app:destination="#id/historyFragment"
app:popUpTo="#id/#id/doorFragment"
app:restoreState="true"
app:popUpToSaveState="true" />
Note that I always used your start destination for popUpTo and also save the backstack state. You don't necessarily need the latter (restoreState and popUpToSaveState) for your use case but maybe want it later down the road when you have multiple fragments for a single drawer entry. This will get you the same behaviour as the current standard drawer integration.
Here's an easy example on how you can use the global action to go to your settings fragment. (The docs also explain how you can use global actions with the SafeArgs Gradle Plugin which is probably the better method.)
navController.navigate(R.id.action_global_settingsFragment)
If you don't want an extra action for every destination and your drawer menu items have the same id as its destination, you can also do it programmatically like this:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
navController.navigate(item.itemId, null, navOptions {
restoreState = true
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
})
return true
}
Basically, I have the following navigation graph:
I want to change my starting point in navigation graph to fragment 2 right after reaching it (in order to prevent going back to fragment 1 when pressing back button - like with the splash screen).
This is my code:
navGraph = navController.getGraph();
navGraph.setStartDestination(R.id.fragment2);
navController.setGraph(navGraph);
But, obviously it's not working and it gets back to fragment 1 after pressing back button.
Am I doing it wrong?
Is there any other solution?
UPDATE:
When you have nav graph like this:
<fragment
android:id="#+id/firstFragment"
android:name="com.appname.package.FirstFragment" >
<action
android:id="#+id/action_firstFragment_to_secondFragment"
app:destination="#id/secondFragment" />
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name="com.appname.package.SecondFragment"/>
And you want to navigate to the second fragment and make it root of your graph, specify the next NavOptions:
NavOptions navOptions = new NavOptions.Builder()
.setPopUpTo(R.id.firstFragment, true)
.build();
And use them for the navigation:
Navigation.findNavController(view).navigate(R.id.action_firstFragment_to_secondFragment, bundle, navOptions);
setPopUpTo(int destinationId, boolean inclusive) - Pop up to a given destination before navigating. This pops all non-matching destinations from the back stack until this destination is found.
destinationId - The destination to pop up to, clearing all intervening destinations.
inclusive - true to also pop the given destination from the back stack.
ALTERNATIVE:
<fragment
android:id="#+id/firstFragment"
android:name="com.appname.package.FirstFragment" >
<action
android:id="#+id/action_firstFragment_to_secondFragment"
app:destination="#id/secondFragment"
app:popUpTo="#+id/firstFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name="com.appname.package.SecondFragment"/>
And then on your code:
findNavController(fragment).navigate(
FirstFragmentDirections.actionFirstFragmentToSecondFragment())
Old answer
Deprecated: The clearTask attribute for actions and the associated API in NavOptions has been deprecated.
Source: https://developer.android.com/jetpack/docs/release-notes
If you want to change your root fragment to fragment 2 (e.g. after pressing back button on fragment 2 you will exit the app), you should put the next attribute to your action or destination:
app:clearTask="true"
Practically it looks in a next way:
<fragment
android:id="#+id/firstFragment"
android:name="com.appname.package.FirstFragment"
android:label="fragment_first" >
<action
android:id="#+id/action_firstFragment_to_secondFragment"
app:destination="#id/secondFragment"
app:clearTask="true" />
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name="com.appname.package.SecondFragment"
android:label="fragment_second"/>
I've added app:clearTask="true" to action.
Now when you perform navigation from fragment 1 to fragment 2 use the next code:
Navigation.findNavController(view)
.navigate(R.id.action_firstFragment_to_secondFragment);
In MainActivity.kt
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(R.navigation.booking_navigation)
if (isTrue){
graph.startDestination = R.id.DetailsFragment
}else {
graph.startDestination = R.id.OtherDetailsFragment
}
val navController = navHostFragment.navController
navController.setGraph(graph, intent.extras)
Remove startDestination from nav_graph.xml
?xml version="1.0" encoding="utf-8"?>
<!-- app:startDestination="#id/oneFragment" -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/navigation_main">
<fragment
android:id="#+id/DetailFragment"
android:name="DetailFragment"
android:label="fragment_detail"
tools:layout="#layout/fragment_detail"/>
<fragment
android:id="#+id/OtherDetailFragment"
android:name="OtherDetailFragment"
android:label="fragment_other_detail"
tools:layout="#layout/fragment_other_detail"/>
</navigation>
I found a solution for this, but it's ugly. I guess this it to be expected with an alpha library, but I hope Google looks into simplifying/fixing this as this is a pretty popular navigation pattern.
Alexey's solution did not work for me. My problem was that I have up arrows showing on my Actionbar by using:
NavigationUI.setupActionBarWithNavController(this, navController)
If I did as Alexey suggests above, my new start fragment still had a arrow pointing to my initial start fragment. If I pressed that up arrow my app would sort-of restart, transitioning to itself (the new start fragment)
Here is the code needed to get to what I wanted which was:
Fragment #1 is where my application initially starts
I can do an Auth check in Fragment #1 and then programmatically change the start to fragment #2.
Once in Fragment #2 there is no up arrow and pressing the back button does not take you to Fragment #1.
Here is the code that accomplishes this. In my Activity's onCreate:
// Setup the toolbar
val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
// Configure the navigation
val navHost = nav_host_fragment as NavHostFragment
graph = navHost.navController
.navInflater.inflate(R.navigation.nav_graph)
graph.startDestination = R.id.welcomeFragment
// This seems to be a magical command. Not sure why it's needed :(
navHost.navController.graph = graph
NavigationUI.setupActionBarWithNavController(this, navHost.navController)
and also:
fun makeHomeStart(){
graph.startDestination = R.id.homeFragment
}
Then in Fragment #1's onActivityCreated, per Alexey's suggestion:
override fun onActivityCreated(savedInstanceState: Bundle?) {
...
// Check for user authentication
if(sharedViewModel.isUserAuthenticated()) {
(activity as MainActivity).makeHomeStart() //<---- THIS is the key
val navOptions = NavOptions.Builder()
.setPopUpTo(R.id.welcomeFragment, true)
.build()
navController.navigate(R.id.action_welcomeFragment_to_homeFragment,null,navOptions)
} else {
navController.navigate(R.id.action_welcomeFragment_to_loginFragment)
}
}
The key code is:
(activity as MainActivity).makeHomeStart() which just runs a method in the activity that changes the graphs startDestination. I could clean this up and turn it into an interface, but I'll wait for Google and hope they improve this whole process. The method 'setPopUpTo' seems poorly named to me and it's not intuitive that your naming the fragment that is getting cut out of the graph. It's also strange to me that they're making these changes in navOptions. I would think navOptions would only relate to the navigation action they're connected to.
And I don't even know what navHost.navController.graph = graph does, but without it the up arrows return. :(
I'm using Navigation 1.0.0-alpha06.
You can also try the followings.
val navController = findNavController(R.id.nav_host_fragment)
if (condition) {
navController.setGraph(R.navigation.nav_graph_first)
} else {
navController.setGraph(R.navigation.nav_graph_second)
}
Instead of trying to pop start destination or navigate manually to target destination, it would be better to have another navigation graph with different workflow.
This would be even better for the case when you want completely different navigation flow conditionally.
You don't really need to pop the Splash Fragment. It can remain there for the rest of your App life. What you should do is from the Splash Screen determine which next Screen to Show.
In the picture above you can ask in the Splash Screen State if there is a saved LoginToken. In case is empty then you navigate to the Login Screen.
Once the Login Screen is done, then you analyze the result save the Token and navigate to your Next Fragment Home Screen.
When the Back Button is Pressed in the Home Screen, you will send back a Result message to the Splash Screen that indicates it to finish the App.
Bellow code may help:
val nextDestination = if (loginSuccess) {
R.id.action_Dashboard
} else {
R.id.action_NotAuthorized
}
val options = NavOptions.Builder()
.setPopUpTo(R.id.loginParentFragment, true)
.build()
findNavController().navigate(nextDestination, null, options)
For those who have a navigation xml file with similar content to this:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/nav_home">
<fragment
android:id="#+id/nav_home"
android:name="HomeFragment"
android:label="#string/menu_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/nav_users"
android:name="UsersFragment"
android:label="#string/users"
tools:layout="#layout/fragment_users" />
<fragment
android:id="#+id/nav_settings"
android:name="SettingsFragment"
android:label="#string/settings"
tools:layout="#layout/fragment_settings" />
</navigation>
suppose current fragment opened is the home fragment and you want to navigate to users fragment, for that just call in the setOnClickListener of the element that you want to navigate to the navigate method from the nav controller similar to this code:
yourElement.setOnClickListener {
view.findNavController().navigate(R.id.nav_users)
}
that will make the app navigate to that other fragment and will also handle the title in the toolbar.
Okay, after messing with this for a bit I found a solution that worked for me that didn't require a ton of work.
It appears two things MUST be in place for it function as if your secondFragment is your start destination.
use the ALTERNATIVE option in the accepted post
<fragment
android:id="#+id/firstFragment"
android:name="com.appname.package.FirstFragment" >
<action
android:id="#+id/action_firstFragment_to_secondFragment"
app:destination="#id/secondFragment"
app:popUpTo="#+id/firstFragment"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/secondFragment"
android:name="com.appname.package.SecondFragment"/>
The above will remove firstFragment from the stack and inflate secondFragment when moving. The app cannot step back to firstFragment anymore BUT your left with secondFragment showing a back arrow as #szaske stated.
This is what made the difference. I previously defined my AppBarConfig using the NavigationController.graph like so
// Old code
val controller by lazy { findNavController(R.id.nav_host_fragment) }
val appBarConfig by lazy { AppBarConfiguration(controller.graph) }
Updating it to define a set of top-level destinations rectified the issue of showing the back arrow on secondFragment instead of a hamburger menu icon.
// secondFragment will now show hamburger menu instead of back arrow.
val appBarConfig by lazy { AppBarConfiguration(setOf(R.id.firstFragment, R.id.secondFragment)) }
Setting the start destination may or may not have negative implications in your project so do it as needed however in this example we do not need to do so. If it makes you warm and fuzzy to ensure that your graph has the correct start fragment defined, you can do it like so.
controller.graph.startDestination = R.id.secondFragment
Note: Setting this does not prevent the back arrow from occurring in secondFragment and from what I have found seems to have no effect on navigation.
I tried to modify code in startDestination.
It works well, but It does not keep the activity, the Navigation component does not restore fragment stack.
I resolved this problem with a dummy startDestination
startDestination is EmptyFragment(just a dummy)
EmptyFragment to FirstFragment action require popUpTo=EmptyFragment and popUpToInclusive=true
NavGraph image
In Activity.onCreate()
if (savedInstanceState == null) {
val navHost = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!
val navController = navHost.findNavController()
if (loginComplete) {
navController.navigate(
R.id.action_emptyFragment_to_FirstFragment
)
} else {
navController.navigate(
R.id.action_emptyFragment_to_WelcomeFragment
)
}
}
when Activity is recreated, savedInstanceState is not null and fragment is restored automatically.
While using Navigation Library, is it okay to finish the inner activity with finish() in onSupportNavigateUp()? Or we should use NavController to remove that Activity from Stack? And, I have trouble using NavController to remove Activity.
My Navigation graph looks like below:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="#id/home_frag">
<fragment
android:id="#+id/home_frag"
android:name="com.yamikrish.app.slicedemo.ui.home.HomeFragment"
android:label="home_frag"
tools:layout="#layout/home_fragment">
<action
android:id="#+id/open_details"
app:destination="#id/details_fragment" />
</fragment>
<activity
android:id="#+id/details_fragment"
android:name="com.yamikrish.app.slicedemo.ui.detail.DetailActivity"
android:label="#string/post_detail"
tools:layout="#layout/detail_page">
<argument
android:name="id"
app:type="integer" />
</activity>
<fragment
android:id="#+id/profile_frag"
android:name="com.yamikrish.app.slicedemo.ui.profile.ProfileFragment"
android:label="#string/profile"
tools:layout="#layout/profile_fragment" />
</navigation>
I have tried to use like below:
(i) Using NavController inside DetailActivity
override fun onSupportNavigateUp(): Boolean {
val nav = NavController(this)
return nav.navigateUp()
}
But getting exception as,
java.lang.IllegalArgumentException: NavController back stack is empty
(ii) Using NavController inside BaseActivity
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.container).navigateUp()
}
That also not working. Am I doing it wrong??
Your detail activity has no concept of the parent nav graph in this case. You example ii is correct. Since BaseActivity contains a navigation graph. I don't know what the DetailActivity is doing but if it also contains a navigation graph then you should use the same code as your BaseActivity. The fact that you're getting that error and also creating a new NavController on the fly tells me that DetailActivity is not using a navigation graph (creating a new NavController and not providing it with a graph will by default initialize with an empty back stack).
In summary, DetailActivity's onSupportNavigateUp should behave like a normal up navigation. If there's a clear parent you should launch it, otherwise, just finish().