So, here is what I want.
The top level data is structured like below :
Section 1 Title -> Section 1 Content (List)
Section 2 Title -> Section 2 Content (List)
.
.
.
Section N Title -> Section N Content (List)
I want to display the section titles in the side menu which opens when the hamburger is icon is selected and when a specific section is selected, the side menu closes and the content for that section is loaded in the screen under the action bar.
So, the relevant UI components to be used are : Toolbar, DrawerLayout (for Side Menu) and Fragment for loading the content in the center.
Now, I am trying to use the Navigation Components and get as much benefits of it as possible. The thing that I am unable to get to work is :
Loading dynamic items in the side menu. The examples show using the android menu resources. I would like to use my own recycler view and load the menu dynamically.
How to define the navGraph for a data structure like this. I tried to create an action from the ContentFragment to itself but I wasn't sure this was right because, there is no action inside it that takes the UI from one ContentFragment to another ContentFragment. It is a top level action from the side menu that loads a different ContentFragment.
Apart from the above 2 questions, I want to know if this is even a right candidate for using navigation components or is it better to use the traditional approach?
Implementing this is easily done with AndroidNavigation components.
1 - Dynamic DrawerLayout can be implemented by adding RecyclerView inside com.google.android.material.navigation.NavigationView
<com.google.android.material.navigation.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="false">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rvDrawer"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.google.android.material.navigation.NavigationView>
2 - Activity navigation init when using DrawerLayout
appBarConfiguration = AppBarConfiguration(
setOf(R.id.mainFragment),
binding.drawerLayout
)
binding.navView.setupWithNavController(navController)
// Setup action bar with nav controller
setupActionBarWithNavController(navController, appBarConfiguration)
3 - com.google.android.material.navigation.NavigationView init
private fun loadDrawerItems(items: Array<String>) {
binding.rvDrawer.apply {
layoutManager = LinearLayoutManager(this#MainActivity)
adapter = DrawerAdapter(items, this#MainActivity)
}
}
4 - DrawerAdapter notify activity when the user clicks on the item
override fun onDrawerItemClick(value: String) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
val direction = NavGraphDirections.refreshMainFragment(value)
navController.navigate(direction)
}
5 - Navigation
<?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/nav_graph"
app:startDestination="#id/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="dh.sos.MainFragment"
android:label="Main fragment">
<argument
android:name="value"
app:argType="string"
android:defaultValue="Default value"/>
</fragment>
<action android:id="#+id/refreshMainFragment"
app:destination="#id/mainFragment"
app:popUpTo="#id/mainFragment"
app:popUpToInclusive="true">
<argument
android:name="value"
app:argType="string"
android:defaultValue="Default value"/>
</action>
</navigation>
app:popUpToInclusive="true" indicates that popUpTo destination should be also removed from the stack, which means every time when we call this action we'll get a new instance of Fragment.
Full source code: https://github.com/dautovicharis/sos_android/tree/q_68441622
I'll start with what I've understood of your question: You have a single Fragment with different data. Now, You want to have a dynamic list (RecyclerView) in the NavigationDrawer and based on that you want to change the data list provided to the fragment ContentFragment. Also, you want to use NavigationGraph of which I actually don't see any purpose as you're only using a single Fragment which can be directly loaded into the FragmentContainer. Now, let's focus on your two points:
Dynamic List in RecyclerView
To create a dynamic list in Drawer, place a RecyclerView in NavigationView as:
<androidx.drawerlayout.widget.DrawerLayout
android:id="#+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:openDrawer="start">
<!--...Other content...-->
<com.google.android.material.navigation.NavigationView
android:id="#+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerViewDrawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/row_drawer" />
</com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>
You can put the RecyclerView in a layout as well, that's your choice.
Create a Layout item and adapter for the RecyclerView to populate. Basic stuff, not including.
Pass a list of the items to the adapter you want to show in the drawer and importantly, pass a callback which can be called when any of the item is clicked. You can set-up an interface or can pass a callback as:
/* You can set the callback to a variable of the AdapterDrawer using the object adapter,
neat way, avoids crash instead of passing in constructor.*/
val adapter = AdapterDrawer(list)
adapter.callback = {position: Int, item: String ->
//This will be called whenever any item is clicked
//and will return the clicked position and the item text.
}
//In the adapter, Create a variable as
var callback: ((position: Int, item: String) -> Unit)? = null
//Call in item's onClickListener as
callback?.invoke(position, itemValue)
This is your first point complete - Setting up a dynamic list in Drawer.
Creating/Re-creating the fragment from Drawer list's callback
Don't use graph property in the layout. It will be set dynamically.
Add the arguments to fragment in the graph as:
<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_classes"
app:startDestination="#id/class_view_pager_fragment">
<fragment
android:id="#+id/class_view_pager_fragment"
android:name="com.tech.cyndi.fragments.FragmentClassViewPager"
tools:layout="#layout/activity_class">
//to set a Model as a type, pass its whole path like com.android.app.DataModel
<argument
android:name="DataList"
app:argType="com.x.x.ModelClass[]" />
</fragment>
</navigation>
Now, put the fragment creation logic in a function which will be called initially to set-up the default fragment and then, later on whenever the callback will be called. The data will then be updated based on the values returned by the callback.
//You can declare and initialize the fragmentHost variable before this.
fun setUpFragment (list: ArrayList<YourModel>) {
val fragmentHost = supportFragmentManager.findFragmentById(binding.yourFragment.id) as NavHostFragment
fragmentHost.navController.apply {
val args = Bundle()
args.putParcelableArrayList("list", list);
setGraph(navInflater.inflate(R.navigation.yourNavGraph), args)
}
Last and Final step, call this function from activity's onCreate() for first time and from the callback declared above whenever the data is changed.
Related
I have multiple fragments like Dashboard,Notifications and Profile in Bottom Navigation. I am using NavGraph- NavController to control the fragments.
I want to save the state of previous fragment.
I don't want to call my API's again on Switching between fragments. Whenever I switch between fragment it calls onDestroyView of previous fragment and onCreateView of current Fragmnent . That's why all the operation in onCreateView or onViewCreated will call again.
How could I get rid of it. Is there any implementation using NavGraph that stop fragment from reCeating
Or is there a way to don't call those API's again . I mean to retatin the UIState.
For example:
The user is on Map fragment and he search some location on Google Map and moves to the next fragment Dashboard
I have tried using
lifecycleScope.launch {
lifecycleScope.launchWhenCreated {
Log.v("LifeCycleState","launchWhenCreated")
}
}
viewLifecycleOwner.lifecycleScope.launch {
lifecycleScope.launchWhenCreated {
Log.v("LifeCycleState","launchWhenCreated in viewLifeCycleOwner")
}
}
You've to follow multi navigations graph concept.
Create separate graphs for the bottom nav icon based. Ex Home, History and Account So you've to create 3 graphs and include in one graph.
Like this
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/nav_graph"
app:startDestination="#+id/home">
<include app:graph="#navigation/home"/>
<include app:graph="#navigation/history"/>
<include app:graph="#navigation/account"/>
Follow this sample
How do I save the state of each bottom navigation fragment while using Android Navigation Component JetPack.
I know there is a way to do it using an Navigation Extension provided by the Android Team - Navigation Extension. - While it works, it requires you to create multiple nav_graph for each fragment and also does not have the back stack I want. Also, switching between fragment seems slow using their approach.
How do I do save the state using a single nav_graph and maintain each back stack.
I am following this tutorial and its working but not saving the state of each fragment. Each instance of the fragment is created on Click of the bottom nav item. - Bottom Nav Tutorial Like Instagram And Youtube
activity_home.xml
<fragment //I get a warning here, when I change to FragmentContainerView, app crashes//
android:id="#+id/nav_host_fragment_2"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#+id/bottom_navigation"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph_2" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?android:attr/windowBackground"
app:itemTextColor="#color/white"
app:itemIconSize = "30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:labelVisibilityMode="unlabeled"
app:menu="#menu/bottom_navigation"/>
menu/bottom_navigation.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/feedRandomFragment"
android:icon="#drawable/home_bottom_nav_selector"
android:title="#string/home"
android:menuCategory="secondary"
/>
<item
android:id="#+id/exploreAndSearchFragment"
android:icon="#drawable/explore_bottom_nav_selector"
android:title="#string/global_explore"
android:menuCategory="secondary"
/>
<item
android:id="#+id/uploadChooseFragment"
android:icon="#drawable/ic_upload"
android:title="#string/upload"
android:menuCategory="secondary"
/>
<item
android:id="#+id/allChallengesFragment"
android:icon="#drawable/challenges_bottom_nav_selector"
android:title="#string/challenges"
android:menuCategory="secondary"
/>
<item
android:id="#+id/profileCurrentUserFragment"
android:icon="#drawable/profile_bottom_nav_selector"
android:title="#string/profile"
android:menuCategory="secondary"
/>
HomeActivity.kt
if(savedInstanceState==null){
setUpBottomNavigationBarBase()
}
private fun setUpBottomNavigationBarBase(){
binding.bottomNavigation.setupWithNavController(Navigation.findNavController(this,
R.id.nav_host_fragment_2))
binding.bottomNavigation.setOnNavigationItemSelectedListener {item ->
onNavDestinationSelected(item, Navigation.findNavController(this, R.id.nav_host_fragment_2))
}
binding.bottomNavigation.itemIconTintList = null
binding.bottomNavigation.setOnNavigationItemReselectedListener {
//do something
}
}
According to the Tutorial, to Maintain backstack, we have to extend all bottom nav fragments from a util class BaseBottomTabFragment which I did and works well.
BaseBottomFragment
open class BaseBottomTabFragment : Fragment() {
var isNavigated = false
fun navigateWithAction(action: NavDirections) {
isNavigated = true
findNavController().navigate(action)
}
fun navigate(resId: Int) {
isNavigated = true
findNavController().navigate(resId)
}
override fun onDestroyView() {
super.onDestroyView()
if (!isNavigated)
requireActivity().onBackPressedDispatcher.addCallback(this) {
val navController = findNavController()
if (navController.currentBackStackEntry?.destination?.id != null) {
findNavController().popBackStackAllInstances(
navController.currentBackStackEntry?.destination?.id!!,
true
)
} else
navController.popBackStack()
}
}
private fun NavController.popBackStackAllInstances(destination: Int, inclusive: Boolean): Boolean {
var popped: Boolean
while (true) {
popped = popBackStack(destination, inclusive)
if (!popped) {
break
}
}
return popped
}
}
So, All my bottom tab fragments extends from that util class - BaseBottomTabFragment like this :
class ExploreAndSearchFragment : BaseBottomTabFragment()
Also, accoriding to the tutorial, to maintain the state of fragment and avoid recreation, each fragment has to have a unique ID which I also did - Sadly, this does not stop the fragment from recreating onClick.
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragments.main.bottomnav.home.view.FeedRandomFragment"
android:fillViewport="true"
android:background="#color/white"
android:id="#+id/homeId">
I set a unique id to All fragment, but it didnt work. Please Help me!
As I had a similar problem, I copied the Navigation Advanced Sample from their GitHub page and started troubleshooting.
My goal was to use multiple back stacks while having an Instagram-like bottom navigation bar. So I wanted to combine MAD Skills' tutorial Navigation: Multiple back stacks with
Furkan Aşkın's Instagram-like back stack guide.
As I implemented both components, the project compiled perfectly but multiple back stacks didn't seem to work as the states of the bottom tabs weren't saved. I double-checked the version to be 2.4.0-alpha01 or newer.
What ended up being the problem was secondary menuCategory defined in menu items:
android:menuCategory="secondary"
This was probably overwriting the multiple back stacks expected behaviour and upon deleting the lines, multiple back stacks work perfectly. Also, redundant back stacks are removed while navigating in different bottom navigation top level fragments.
The issue I encountered is related to setting navigation graph programatically. What I want to achieve is to decide in Activity which fragment should be a start destination. What's more, each of these fragments have additional arguments.
Let's say we have such 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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph">
<fragment
android:id="#+id/fragment1"
android:name="com.mypackage.Fragment1"
tools:layout="#layout/layout_fragment1">
<argument
android:name="argument1"
app:argType="int" />
</fragment>
<fragment
android:id="#+id/fragment2"
android:name="com.mypackage.Fragment2"
tools:layout="#layout/layout_fragment2">
<argument
android:name="argument2"
app:argType="string" />
</fragment>
</navigation>
If we use Safe Args plugin (androidx.navigation.safeargs.kotlin), two classes will be generated: Fragment1Args and Fragment2Args, both implementing NavArgs interface.
Now because we need to decide which fragment should became start destination, we need to make graph programatically:
val graph = navController.navInflater.inflate(R.navigation.nav_graph)
if (someCondition) {
graph.startDestination = R.id.fragment1
} else {
graph.startDestination = R.id.fragment2
}
navController.graph = graph
But Fragment1 and Fragment2 requires some additional arguments, so we need somehow to add them to graph. It turns out that graph has method addArguments(NavArguments), but as you see it's not NavArgs interface. Question is how to set these arguments properly that Fragment1 or Fragment2 will be able to extract?
I'm using 1.0.0-rc02 version of navigation framework.
When you add <argument> tag to your navigation XML, it is already creating the NavArgument classes when you call inflate(), so there's nothing you need to do to the graph in order to pass arguments to the start destination.
Instead, you should use setGraph(NavGraph, Bundle) to set the graph and pass initial arguments to the start destination.
// Construct your Bundle of arguments
val bundle = bundleOf()
// set the graph with specific arguments for the start destination
navController.setGraph(graph, bundle)
You said: "What I want to achieve is to decide in Activity which fragment should be a start destination."
And I think you're trying to make the decision in the fragmen, which is not correct here because you didn't open the fragment yet, Thus the correct behavior is to do that in activity e.g:
when (someCondition) {
TO_FRAGMENT_1-> {
findNavController(R.id.fragment).navigate(R.id.fragment1)
}
TO_FRAGMENT_2-> {
findNavController(R.id.fragment).navigate(R.id.fragment2)
}
}
And if the activity is the first activity in your app, you should use something else instead of args, e.g: SharedPref.
my project file: https://drive.google.com/file/d/11llz7ylWe7ACyLMBbqp6YzugUL8hhImt/view?usp=sharing
so I have 2 navigation graph. called main navigation graph and also auth graph.
I include main graph into auth graph and vice versa, auth graph in main graph.
I want to implement login system, so when the user successfully logged in then the user will go to main activity (that has bottom navigation view and toolbar), auth activity does not have bottom navigation view or fragment. here is the graphs
main 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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/navigation_graph"
app:startDestination="#id/destination_home">
<include app:graph="#navigation/auth_graph" />
<fragment android:id="#+id/destination_home" android:name="com.muchammadagunglaksana.navcontroller.HomeFragment"
android:label="Home Judul" tools:layout="#layout/fragment_home">
<action android:id="#+id/action_toAuthActivity" app:destination="#id/auth_graph"/>
</fragment>
<fragment android:id="#+id/destination_camera" android:name="com.muchammadagunglaksana.navcontroller.CameraFragment"
android:label="Camera Judul" tools:layout="#layout/fragment_camera">
<action android:id="#+id/toPhotosDestination" app:destination="#id/destination_photos"/>
</fragment>
<fragment android:id="#+id/destination_photos" android:name="com.muchammadagunglaksana.navcontroller.PhotosFragment"
android:label="Foto Judul" tools:layout="#layout/fragment_photos">
<action android:id="#+id/toHomeDestination" app:destination="#id/destination_home"/>
<argument android:name="numberOfPhotos" app:argType="integer" android:defaultValue="0"/>
</fragment>
<fragment android:id="#+id/destination_settings"
android:name="com.muchammadagunglaksana.navcontroller.SettingsFragment"
android:label="Setting Judul" tools:layout="#layout/fragment_settings"/>
</navigation>
Auth graph:
<include app:graph="#navigation/navigation_graph" />
<fragment android:id="#+id/loginFragment" android:name="com.muchammadagunglaksana.navcontroller.LoginFragment"
android:label="fragment_login" tools:layout="#layout/fragment_login">
<action android:id="#+id/action_toMainActivity" app:destination="#id/navigation_graph"/>
</fragment>
when login button clicked in the LoginFragment then I use the code below:
login_button.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_toMainActivity)
}
and also in the HomeFragment, when the logout button did clicked I use:
logout_button.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_toAuthActivity)
}
but I got stackoverflowerror:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.muchammadagunglaksana.navcontroller, PID: 14322
java.lang.StackOverflowError: stack size 8MB
at android.support.v4.util.SparseArrayCompat.(SparseArrayCompat.java:77)
at android.support.v4.util.SparseArrayCompat.(SparseArrayCompat.java:62)
at androidx.navigation.NavGraph.(NavGraph.java:44)
at androidx.navigation.NavGraphNavigator.createDestination(NavGraphNavigator.java:54)
at androidx.navigation.NavGraphNavigator.createDestination(NavGraphNavigator.java:29)
at androidx.navigation.NavInflater.inflate(NavInflater.java:100)
at androidx.navigation.NavInflater.inflate(NavInflater.java:80)
at androidx.navigation.NavInflater.inflate(NavInflater.java:128)
at androidx.navigation.NavInflater.inflate(NavInflater.java:80)
at androidx.navigation.NavInflater.inflate(NavInflater.java:128)
at androidx.navigation.NavInflater.inflate(NavInflater.java:80)
at androidx.navigation.NavInflater.inflate(NavInflater.java:128)
at androidx.navigation.NavInflater.inflate(NavInflater.java:80)
at androidx.navigation.NavInflater.inflate(NavInflater.java:128)
at androidx.navigation.NavInflater.inflate(NavInflater.java:80)
at androidx.navigation.NavInflater.inflate(NavInflater.java:128)
na.navcontroller E/JavaBinder: !!! FAILED BINDER TRANSACTION !!!
what went wrong ?
An <include> tag is the exact equivalent of copy/pasting the exact content of the including graph in place of the <include>. By having your auth_graph include the navigation_graph, you've built a loop: navigation_graph contains auth_graph which contains navigation_graph on and on forever.
What you need to do is remove the <include app:graph="#navigation/navigation_graph" /> from your auth_graph. Because your auth_graph is already within the navigation_graph, you don't need to add it a second time, but you can reference any of those destinations directly.
As #ianhanniballake said when you use an <include> tag you copy all the navgraph destinations into the actual one. I had the same problem so what I did was this. I created a util class where I have this method:
/**
* Search all the destinations in
* the graph to be added. If the
* actual graph doesn't contain
* one of these destinations, is
* added to the actual graph
*
* #param view the actual view (to extract the actual graph and to inflate the new one)
* #param navGraphId the graph destinations to be added
*/
fun addGraphDestinations(view: View, navGraphId : Int) {
// Get the actual navcontroller
val navController = view.findNavController()
// Get the nav inflater
val navInflater = navController.navInflater
// Get the actual graph in use
val actualGraph = navController.graph
// Inflate the new graph
val newGraph = navInflater.inflate(navGraphId)
val list = mutableListOf<NavDestination>()
// Search if there's a new destination to add into the actual graph
newGraph.forEach { destination ->
if(actualGraph.findNode(destination.id) == null) {
list.add(destination)
}
}
list.forEach {
newGraph.remove(it)
actualGraph.addDestination(it)
}
}
So when it comes the case where you need to add a graph, you add it in code like this:
// We have to check if all prospect destinations are already added to the actual graph
NavigationUtils.addGraphDestinations(view, R.navigation.your_graph)
Hope it helps someone!
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.