I was trying out Navigation architecture component and is now having difficulties in setting the title. How do I set the title programmatically and also how it works?
To clear my question, let's have an example, where, I've set up a simple app with MainActivity hosting the navigation host controller, the MainFragment has a button and on clicking the button it goes to DetailFragment.
The same code from another question of multiple app bars on stack-overflow.
MainActivity
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Setting up a back button
NavController navController = Navigation.findNavController(this, R.id.nav_host);
NavigationUI.setupActionBarWithNavController(this, navController);
}
#Override
public boolean onSupportNavigateUp() {
return Navigation.findNavController(this, R.id.nav_host).navigateUp();
}
}
MainFragment
public class MainFragment extends Fragment {
public MainFragment() {
// Required empty public constructor
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_main, container, false);
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
Button buttonOne = view.findViewById(R.id.button_one);
buttonOne.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.detailFragment));
}
}
DetailFragment
public class DetailFragment extends Fragment {
public DetailFragment() {
// Required empty public constructor
}
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_detail, container, false);
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="#+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top"
android:layout_marginTop="?android:attr/actionBarSize"
app:defaultNavHost="true"
app:layout_anchor="#id/bottom_appbar"
app:layout_anchorGravity="top"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
app:navGraph="#navigation/mobile_navigation" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="#+id/bottom_appbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:layout_gravity="bottom" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="#id/bottom_appbar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
navigation.xml
<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/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.MainFragment"
android:label="fragment_main"
tools:layout="#layout/fragment_main" >
<action
android:id="#+id/toAccountFragment"
app:destination="#id/detailFragment" />
</fragment>
<fragment
android:id="#+id/detailFragment"
android:name="com.example.DetailFragment"
android:label="fragment_account"
tools:layout="#layout/fragment_detail" />
</navigation>
So when start my app, the title is "MainActivity". As usual it shows the MainFragment that contains the button to go to DetailFragment. In the DialogFragment I've set the title as:
getActivity().getSupportActionBar().setTitle("Detail");
First Problem: So clicking the button on the MainFragment to goto DetailFragment, it does go there and the title changes to "Detail". But on clicking the back button, the title changes to "fragment_main". So I added this line of code to MainFragment:
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// ...
//Showing the title
Navigation.findNavController(view)
.getCurrentDestination().setLabel("Hello");
}
Now the while returning back from DetailFragment to MainFragment the title changes to "Hello". But here comes the second problem, when I close the app and start again, the title changes back to "MainActivity" though it should be showing "Hello" instead, know?
Ok, then adding setTitle("Hello") in MainFrgment is not working too. For example, the activity starts and the title is "Hello", go to DetailsFragment and press the back button again, the title goes back to "fragment_main".
The only solution is to have both setTitle("Hello") along with Navigation.findNavController(view).getCurrentDestination().setLabel("Hello") in MainFragment.
So what is the proper way to show the title for fragments using Navigation Component?
It's actually because of:
android:label="fragment_main"
Which you have set in the xml.
So what is the proper way to show the title for Fragments using
Navigation Component?
setTitle() works at this point. But, because you set label for those Fragments, it might show the label again when recreating the Activity. The solution will probably be deleting android:label and then do your things with code:
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle("your title");
Or:
((AppCompatActivity) getActivity()).getSupportActionBar().setSubtitle("your subtitle");
In onCreateView().
Found a workaround:
interface TempToolbarTitleListener {
fun updateTitle(title: String)
}
class MainActivity : AppCompatActivity(), TempToolbarTitleListener {
...
override fun updateTitle(title: String) {
binding.toolbar.title = title
}
}
Then:
(activity as TempToolbarTitleListener).updateTitle("custom title")
Check this out too:Dynamic ActionBar title from a Fragment using AndroidX Navigation
As others are still participating in answering this question, let me answer my own question as APIs has changed since then.
First, remove android:label from the fragment/s that you wish to change the title of, from within navigation.xml (aka Navigation Graph),.
Now you can change the title from with the Fragment by calling
(requireActivity() as MainActivity).title = "My title"
But the preferred way you should be using is the API NavController.addOnDestinationChangedListener from within MainActivity. An Example:
NavController.OnDestinationChangedListener { controller, destination, arguments ->
// compare destination id
title = when (destination.id) {
R.id.someFragment -> "My title"
else -> "Default title"
}
// if (destination == R.id.someFragment) {
// title = "My title"
// } else {
// title = "Default Title"
// }
}
You can use this code in your fragment if you don't specify your app bar(default appbar)
(activity as MainActivity).supportActionBar?.title = "Your Custom Title"
Remember to delete the android:label attribute in your navigation graph
Happy code ^-^
From experience, NavController.addOnDestinationChangedListener
Seems to perform well. My example below on my MainActivity did the magic
navController.addOnDestinationChangedListener{ controller, destination, arguments ->
title = when (destination.id) {
R.id.navigation_home -> "My title"
R.id.navigation_task_start -> "My title2"
R.id.navigation_task_finish -> "My title3"
R.id.navigation_status -> "My title3"
R.id.navigation_settings -> "My title4"
else -> "Default title"
}
}
There is a much easier way to achieve this nowadays with Kotlin and androidx.navigation:navigation-ui-ktx:
import androidx.navigation.ui.setupActionBarWithNavController
class MainActivity : AppCompatActivity() {
private val navController: NavController
get() = findNavController(R.id.nav_host_fragment)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
setSupportActionBar(binding.toolbar)
setupActionBarWithNavController(navController) // <- the most important line
}
// Required by the docs for setupActionBarWithNavController(...)
override fun onSupportNavigateUp() = navController.navigateUp()
}
That's basically it. Don't forget to specify android:label in your nav graphs.
You can use the navigation graph xml file and set the label of the fragment to an argument.
Then, in your parent fragment, you can pass an argument using SafeArgs (please, follow the guide on https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args to set up SafeArgs) and provide a default value to avoid the title being null or empty.
<!--this is originating fragment-->
<fragment
android:id="#+id/fragmentA"
android:name=".ui.FragmentA"
tools:layout="#layout/fragment_a">
<action
android:id="#+id/fragmentBAction"
app:destination="#id/fragmentB" />
</fragment>
<!--set the fragment's title to a string passed as an argument-->
<!--this is a destination fragment (assuming you're navigating FragmentA to FragmentB)-->
<fragment
android:id="#+id/fragmentB"
android:name="ui.FragmentB"
android:label="{myTitle}"
tools:layout="#layout/fragment_b">
<argument
android:name="myTitle"
android:defaultValue="defaultTitle"
app:argType="string" />
</fragment>
In originating Fragment:
public void onClick(View view) {
String text = "text to pass as a title";
FragmentADirections.FragmentBAction action = FragmentADirections.fragmentBAction();
action.setMyTitle(text);
Navigation.findNavController(view).navigate(action);
}
FragmentADirections and FragmentBAction -
These classes are autogenerated from IDs in your nav_graph.xml file.
In 'your action ID + Action' type classes you can find auto-generated setter methods, which you can use to pass arguments
In your receiving destination, you call auto-generated class {your receiving fragment ID}+Args
FragmentBArgs.fromBundle(requireArguments()).getMyTitle();
Please refer to the official guide at
https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args
NavigationUI.setupActionBarWithNavController(this, navController)
Don't forget to specify android:label for your fragments in your nav graphs.
To navigate back:
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, null)
}
I've spent a couple of hours trying to do a very simple thing such as changing the bar title of a detail fragment when navigating from a particuler item on a master fragment. My daddy always said to me: K.I.S.S. Keep it simple son.
setTitle() crashed, interfaces are cumbersome, came up with a (very) simple solution in terms of lines of code, for the latest fragment navigation implementation.
In the master fragment resolve the NavController, get the NavGraph, find the destination Node, set the Title, and last but not least navigate to it:
//----------------------------------------------------------------------------------------------
private void navigateToDetail() {
NavController navController = NavHostFragment.findNavController(FragmentMaster.this);
navController.getGraph().findNode(R.id.FragmentDetail).setLabel("Master record detail");
navController.navigate(R.id.action_FragmentMaster_to_FragmentDetail,null);
}
The API may have changed since the question was asked, but now you may indicate a reference to a string in your app resources in the navigation graph label.
This is pretty helpful if you want a static title for your fragments.
delete detatil fragment's label in navigation graph xml file.
then pass the prefered title through arguments to destination fragment which needs title like so.
The First Fragment code - Start point
findNavController().navigate(
R.id.action_FirstFragment_to_descriptionFragment,
bundleOf(Pair("toolbar_title", "My Details Fragment Title"))
)
as you see I sent as pair in arguments bundle when navigating to Destination Fragment
so in your Destination Fragment get title from arguments in onCreate method like this
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbarTitle = requireArguments().getString("toolbar_title", "")
}
then use it to change Main Activity's title in onCreateView method like this
requireActivity().toolbar.title = toolbarTitle
I would suggest to include AppBar in each screen.
To avoid code duplicates, create a helper, that builds AppBar, taking the title as a parameter. Then invoke the helper in each screen class.
Another approach could be this:
fun Fragment.setToolbarTitle(title: String) {
(activity as NavigationDrawerActivity).supportActionBar?.title = title
}
Update title with either label in navigation xml or exclude labels and set with requiresActivity().title Supports mixing the two ways for screens with and without dynamic titles. Works for me with a Compose UI toolbar and Tabs.
val titleLiveData = MutableLiveData<String>()
findNavController().addOnDestinationChangedListener { _, destination, _ ->
destination.label?.let {
titleLiveData.value = destination.label.toString()
}
}
(requireActivity() as AppCompatActivity).setSupportActionBar(object: Toolbar(requireContext()) {
override fun setTitle(title: CharSequence?) {
titleLiveData.value = title.toString()
}
})
titleLiveData.observe(viewLifecycleOwner, {
// Update your title
})
android:label="{title_action}"
<?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/FirstFragment">
<fragment
android:id="#+id/FirstFragment"
android:name="com.example.contact2.FirstFragment"
android:label="#string/first_fragment_label"
tools:layout="#layout/fragment_first">
<action
android:id="#+id/action_FirstFragment_to_SecondFragment"
app:destination="#id/SecondFragment"
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>
<fragment
android:id="#+id/SecondFragment"
android:name="com.example.contact2.SecondFragment"
android:label="{title_action}"
tools:layout="#layout/fragment_second">
<action
android:id="#+id/action_SecondFragment_to_FirstFragment"
app:destination="#id/FirstFragment"
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" />
<argument
android:name="title_action"
app:argType="string"
app:nullable="true" />
</fragment>
</navigation>
Anyway few of those answers I tried did not work for then I decided to do it the old Java way in Kotlin using interface
Created an interface as shown below.
interface OnTitleChangeListener {
fun onTitleChange(app_title: String)
}
Then made my activity to implement this interface as shown below.
class HomeActivity : AppCompatActivity(), OnTitleChangeListener {
override fun onTitleChange(app_title: String) {
title = app_title
}
}
How on my fragment's on activity attache I this this
override fun onAttach(context: Context) {
super.onAttach(context)
this.currentContext = context
(context as HomeActivity).onTitleChange("New Title For The app")
}
The simple solution:
Layout
androidx.coordinatorlayout.widget.CoordinatorLayout
com.google.android.material.appbar.AppBarLayout
com.google.android.material.appbar.MaterialToolbar
androidx.constraintlayout.widget.ConstraintLayout
com.google.android.material.bottomnavigation.BottomNavigationView
fragment
androidx.navigation.fragment.NavHostFragment
Activity
binding = DataBindingUtil.setContentView(this, layoutRes)
setSupportActionBar(binding.toolbar)
val controller = findNavController(R.id.nav_host)
val configuration = AppBarConfiguration(
setOf(
R.id.navigation_home,
R.id.navigation_dashboard,
R.id.navigation_notifications
)
)
setupActionBarWithNavController(controller, configuration)
navView.setupWithNavController(controller)
It could be helpful if you would like to change the title of Toolbar programmatically with low cohesion code between Activity and Fragment.
class DetailsFragment : Fragment() {
interface Callbacks {
fun updateTitle(title: String)
}
private var listener: Callbacks? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// keep activity as interface only
if (context is Callbacks) {
listener = context
}
}
override fun onDetach() {
// forget about activity
listener = null
super.onDetach()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
inflater.inflate(R.layout.fragment_details, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listener?.updateTitle("Dynamic generated title")
}
}
class MainActivity : AppCompatActivity(), DetailsFragment.Callbacks {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun updateTitle(title: String) {
supportActionBar?.title = title
}
}
September 4, 2021
Using Kotlin in my case this is solution. use this code in MainActivity
val navController = this.findNavController(R.id.myNavHostFragment)
navController.addOnDestinationChangedListener { controller, destination, arguments ->
destination.label = when (destination.id) {
R.id.homeFragment -> resources.getString(R.string.app_name)
R.id.roomFloorTilesFragment -> resources.getString(R.string.room_floor_tiles)
R.id.roomWallTilesFragment -> resources.getString(R.string.room_wall_tiles)
R.id.ceilingRateFragment -> resources.getString(R.string.ceiling_rate)
R.id.areaConverterFragment -> resources.getString(R.string.area_converter)
R.id.unitConverterFragment -> resources.getString(R.string.unit_converter)
R.id.lenterFragment -> resources.getString(R.string.lenter_rate)
R.id.plotSizeFragment -> resources.getString(R.string.plot_size)
else -> resources.getString(R.string.app_name)
}
}
NavigationUI.setupActionBarWithNavController(...)
NavigationUI.setupWithNavController(...)
Related
I have a fragment that can be either navigated to via a bottom navBar without arguments or from a different fragment with arguments. The navigation itself works perfectly fine but as soon as I add my arguments it crashes.
I'm unsure how much code is needed, so sorry if it's not complete:
This is my navigation fragment:
<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"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="fh.wfp2.flatlife.ui.views.HomeFragment"
android:label="Flatlife"
tools:layout="#layout/home_fragment" />
<fragment
android:id="#+id/todoFragment"
android:name="fh.wfp2.flatlife.ui.views.TodoFragment"
android:label="Todos">
<action
android:id="#+id/action_todoFragment_to_addTodoFragment"
app:destination="#id/addTodoFragment" />
<argument
android:name="taskname"
app:argType="string" />
<argument
android:name="isImportant"
app:argType="boolean" />
</fragment>
<fragment
android:id="#+id/addTodoFragment"
android:name="fh.wfp2.flatlife.ui.views.AddTodoFragment"
android:label="AddTodoFragment">
<action
android:id="#+id/action_addTodoFragment_to_todoFragment"
app:destination="#id/todoFragment" />
</fragment>
</navigation>
This is my addTodoFragment:
class AddTodoFragment : Fragment(R.layout.add_task_fragment) {
private lateinit var binding: AddTaskFragmentBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = AddTaskFragmentBinding.bind(view)
binding.bAddTodo.setOnClickListener {
if (binding.etAddTodo.text.isNotEmpty()) {
// viewModel.onAddTodoClicked(binding.etAddTodo.text.toString(),
binding.cbImportant.isChecked)
findNavController().navigate(
AddTodoFragmentDirections.actionAddTodoFragmentToTodoFragment(
binding.etAddTodo.text.toString(), binding.cbImportant.isChecked
)
)
} else {
Snackbar.make(it, "The task field can't be empty", Snackbar.LENGTH_SHORT).show()
}
}
}
}
This is how I'm trying to get the arguments in TodoFragment.
private val args: TodoFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//setting binding and other onClickListeners but they work fine
//reading args
//this i thought would work because then args only get read if not null
args?.let { //debugger stops here and app crashes
if(args.taskname.isNotEmpty())
viewModel.onAddTodoClick(Todo(name = args.taskname, isImportant = args.isImportant))
}
/*second try which I took directly from developers.android but which doesn't make sense in my case i
think because the args could be null I guess
val todoName = args.taskname
val isImportant = args.isImportant
viewModel.onAddTodoClick(Todo(name = todoName, isImportant = isImportant))
*/
}
This is the error message i get:
I hope it is clear what I mean, otherwise, I'll update the question. It'll probably be something simple but I can't quite put my finger on it.
So credit to #Onik for the idea to leave SafeArgs away. I needed to dump the SafeArgs class call for receiving the arguments and it looks like this now. This is definitely not the cleanest way I guess but I don't know yet why the SafeArgs call doesn't work and I'm too new to Kotlin to write the following code in a cleaner way. But it works, for now.
arguments?.let {
var myArg1: String? = null
var myArg2: Boolean
arguments?.getString("argument1")?.let { arg1->
myArg1= arg1
}
arguments?.getBoolean("argument2")?.let { arg2->
myArg2= arg2
if (myArg1!= null)
//do logis with args
}
}
val args = arguments?.let {
SecondFragmentArgs.fromBundle(
it
)
}
if (args != null) {
firstDataList = args.taskName
secondDataList = args.isImportant
}
This is how the receiving fragment code should look like. This should work.
I'm implementing this bottom navigation pattern but with a splash fragment.
My issue is when I navigate throw different fragments with bottom menu and I press to go back, I don't go back to the home fragment, instead of this, I return to the previous fragment.
For example, I have fragments A-B-C:
Now I'm on fragment A and I press to go to B.
Then I press to go to C (from B).
Then I press to go back.
The result is I'm getting back to B, not to fragment A (what I really want!).
(If I set in the navigation graph app:startDestination -> fragment A - instead of login fragment - everything goes well).
Here is my graph:
<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/main_navigation"
app:startDestination="#id/splashFragment">
<fragment
android:id="#+id/splashFragment"
android:name="application.SplashFragment"
tools:layout="#layout/fragment_splash">
<action
android:id="#+id/action_splashFragment_to_fragmentA"
app:destination="#id/fragmentA"
app:popUpTo="#id/main_navigation"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/fragmentA"
android:name="application.fragmentA"
android:label="#string/fragmentA"
tools:layout="#layout/fragmentA" />
<fragment
android:id="#+id/fragmentB"
android:name="application.fragmentB"
android:label="fragmentB"
tools:layout="#layout/fragmentB" />
<fragment
android:id="#+id/fragmentC"
android:name="application.fragmentC"
android:label="#string/fragmentC"
tools:layout="#layout/fragmentC" />
And here my MainActivity:
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Toolbar & Navigation
setSupportActionBar(toolbar)
navController = findNavController(R.id.nav_host)
// AppBarConfiguration with the correct top-level destinations
appBarConfiguration = AppBarConfiguration(
topLevelDestinationIds = setOf(
R.id.fragmentA,
R.id.fragmentB,
R.id.fragmentC
)
)
// This allows NavigationUI to decide what label to show in the action bar
// By using appBarConfig, it will also determine whether to
// show the up arrow or drawer menu icon
setupActionBarWithNavController(navController, appBarConfiguration)
// Set destinations to left and bottom navigation
bottom_navigation.setupWithNavController(navController)
// Set visibility for NavigationView & Toolbar
visibilityNavElements()
}
// Allows NavigationUI to support proper up navigation or the drawer layout
// drawer menu, depending on the situation
override fun onSupportNavigateUp() = navController.navigateUp(appBarConfiguration)
private fun visibilityNavElements() {
findNavController(R.id.nav_host).addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.splashFragment -> {
toolbar.visibility = View.GONE
bottom_navigation.visibility = View.GONE
}
else -> {
toolbar.visibility = View.VISIBLE
bottom_navigation.visibility = View.VISIBLE
}
}
}
}
}
Thanks!
You can handle OnbackPress in any fragment like below and is the recommended way . You can use it in Fragment C and when back is pressed you can navigate to Fragment A
in Java
OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
#Override
public void handleOnBackPressed() {
}
};
requireActivity().getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
in Kotlin
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// Handle the back button event
}
This problem has been asked a few times now, but we are in 2020 now, did anyone find a good usable solution to this yet?
I want to be able to navigate using the bottom navigation control without refreshing the fragment each time they are selected. Here is what I have currently:
navigation/main.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/main"
app:startDestination="#id/home">
<fragment
android:id="#+id/home"
android:name="com.org.ftech.fragment.HomeFragment"
android:label="#string/app_name"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/news"
android:name="com.org.ftech.fragment.NewsFragment"
android:label="News"
tools:layout="#layout/fragment_news"/>
<fragment
android:id="#+id/markets"
android:name="com.org.ftech.fragment.MarketsFragment"
android:label="Markets"
tools:layout="#layout/fragment_markets"/>
<fragment
android:id="#+id/explore"
android:name="com.org.ftech.ExploreFragment"
android:label="Explore"
tools:layout="#layout/fragment_explore"/>
</navigation>
activity_mail.xml:
<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout
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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#+id/bottomNavigationView"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/main" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemIconTint="#color/nav"
app:itemTextColor="#color/nav"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/main">
</com.google.android.material.bottomnavigation.BottomNavigationView>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.navigation.NavigationView
app:menu="#menu/main"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="#+id/navigationView"
android:layout_gravity="start">
</com.google.android.material.navigation.NavigationView>
</androidx.drawerlayout.widget.DrawerLayout>
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private var drawerLayout: DrawerLayout? = null
private var navigationView: NavigationView? = null
private var bottomNavigationView: BottomNavigationView? = null
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
drawerLayout = findViewById(R.id.drawer_layout)
navigationView = findViewById(R.id.navigationView)
bottomNavigationView = findViewById(R.id.bottomNavigationView)
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(setOf(R.id.markets, R.id.explore, R.id.news, R.id.home), drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
findViewById<NavigationView>(R.id.navigationView)
.setupWithNavController(navController)
findViewById<BottomNavigationView>(R.id.bottomNavigationView)
.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.search) {
startActivity(Intent(applicationContext, SearchableActivity::class.java))
}
return super.onOptionsItemSelected(item)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.options_menu, menu)
return super.onCreateOptionsMenu(menu)
}
}
In the fragment I am making a few calls to my services to fetch the data in onCreateView, when resuming the fragment I am assuming those calls will not longer be executed and the state of the fragment should be preserved.
Try this:
public class MainActivity extends AppCompatActivity {
final Fragment fragment1 = new HomeFragment();
final Fragment fragment2 = new DashboardFragment();
final Fragment fragment3 = new NotificationsFragment();
final FragmentManager fm = getSupportFragmentManager();
Fragment active = fragment1;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
fm.beginTransaction().add(R.id.main_container, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.main_container, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.main_container,fragment1, "1").commit();
}
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
fm.beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
return true;
case R.id.navigation_dashboard:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;
return true;
case R.id.navigation_notifications:
fm.beginTransaction().hide(active).show(fragment3).commit();
active = fragment3;
return true;
}
return false;
}
};
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return super.onCreateOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
startActivity(new Intent(MainActivity.this, SettingsActivity.class));
return true;
}
return super.onOptionsItemSelected(item);
}
}
Or You can follow Google's recommended solution: Google Link
The simple solution to stop refreshing on multiple clicks on the same navigation item could be
binding.navView.setOnNavigationItemSelectedListener { item ->
if(item.itemId != binding.navView.selectedItemId)
NavigationUI.onNavDestinationSelected(item, navController)
true
}
where binding.navView is the reference for BottomNavigationView using Android Data Binding.
Kotlin 2020 Google's Recommended Solution
Many of these solutions call the Fragment constructor in the Main Activity. However, following Google's recommended pattern, this is not needed.
Setup Navigation Graph Tabs
Firstly create a navigation graph xml for each of your tabs under the res/navigation directory.
Filename: tab0.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/tab0"
app:startDestination="#id/fragmentA"
tools:ignore="UnusedNavigation">
<fragment
android:id="#+id/fragmentA"
android:label="#string/fragment_A_title"
android:name="com.app.subdomain.fragA"
>
</fragment>
</navigation>
Repeat the above template for your other tabs. Important all fragments and the navigation graph has an id (e.g. #+id/tab0, #+id/fragmentA).
Setup Bottom Navigation View
Ensure the navigation ids are the same as the ones specified on the bottom menu xml.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:title="#string/fragment_A_title"
android:id="#+id/tab0"
android:icon="#drawable/ic_baseline_book_24"/>
<item android:title="#string/fragment_B_title"
android:id="#+id/tab1"
android:icon="#drawable/ic_baseline_add_alert_24"/>
<item android:title="#string/fragment_C_title"
android:id="#+id/tab2"
android:icon="#drawable/ic_baseline_book_24"/>
<item android:title="#string/fragment_D_title"
android:id="#+id/tab3"
android:icon="#drawable/ic_baseline_more_horiz_24"/>
</menu>
Setup Activity Main XML
Ensure FragmentContainerView is being used and not <fragment and do not set the app:navGraph attribute. This will set later in code
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#+id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/main_toolbar"
/>
Main Activity XML
Copy over the following Code into your main activity Kotlin file and call setupBottomNavigationBar within OnCreateView. Ensure you navGraphIds use R.navigation.whatever and not R.id.whatever
private lateinit var currentNavController: LiveData<NavController>
private fun setupBottomNavigationBar() {
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottomNavigationView)
val navGraphIds = listOf(R.navigation.tab0, R.navigation.tab1, R.navigation.tab2, R.navigation.tab3)
val controller = bottomNavigationView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.fragmentContainerView,
intent = intent
)
controller.observe(this, { navController ->
val toolbar = findViewById<Toolbar>(R.id.main_toolbar)
val appBarConfiguration = AppBarConfiguration(navGraphIds.toSet())
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
setSupportActionBar(toolbar)
})
currentNavController = controller
}
override fun onSupportNavigateUp(): Boolean {
return currentNavController?.value?.navigateUp() ?: false
}
Copy NavigationExtensions.kt File
Copy the following file to your codebase
[EDIT] The above link is broken. Found it in a forked repo
Source
Google's Solution
If you are using Jetpack, the easiest way to solve this is using ViewModel
You have to save all valuable data and not make unnecessary database loads or network calls everytime you go to a fragment from another.
UI controllers such as activities and fragments are primarily intended to display UI data, react to user actions, or handle operating system communication, such as permission requests.
Here is when we use ViewModels
ViewModel objects are automatically retained during configuration changes so that data they hold is immediately available to the next activity or fragment instance.
So if the fragment is recreated, all your data will be there instantly instead of make another call to database or network. Its important to know that if the activity or fragment that holds the ViewModel is reacreated, you will receive the same ViewModel instance created before.
But in this case you have to specify the ViewModel to have activity scope instead of fragment scope, independently if you are using a shared ViewModel for all the fragments, or a different ViewModel for every fragment.
Here is a little example using LiveData too:
//Using KTX
val model by activityViewModels<MyViewModel>()
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
// update UI
})
//Not using KTX
val model by lazy {ViewModelProvider(activity as ViewModelStoreOwner)[MyViewModel::class.java]}
model.getData().observe(viewLifecycleOwner, Observer<DataModel>{ data ->
// update UI
})
And that's it! Google is actively working on multiple back stack support for bottom tab Navigation and claim that it'll arrive on Navigation 2.4.0 as said here and on this issue tracker if you want and/or your problem is more related to multiple back stack, you can check out those links
Remember fragments still be recreated, usually you don't change component behavior, instead, you adapt your data to them!
I leave you some useful links:
ViewModel Overview Android Developers
How to communicate between fragments and activities with ViewModels - on Medium
Restoring UI State using ViewModels - on Medium
Quick tip, if you just want to prevent loading the already selected fragment just override setOnNavigationItemReselectedListener and do nothing, but this won't save the fragment states
binding.navBar.setOnNavigationItemReselectedListener { }
You use the old version, you just use version 2.4.0-alpha05 or above.
This answer is updated in 2021.
androidx.navigation:navigation-runtime-ktx:2.4.0-alpha05
androidx.navigation:navigation-fragment-ktx:2.4.0-alpha05
androidx.navigation:navigation-ui-ktx:2.4.0-alpha05
If you use NavigationUI.setupWithNavController(), the NavOptions are defined for you with NavigationUI.onNavDestinationSelected(). These options include launchSingleTop and, if the menu item is not secondary, a popUpTo the root of the graph.
The problem is, that launchSingleTop still replaces the top fragment with a new one. To resolve this issue, you'd have to create your own setupWithNavController() and onNavDestinationSelected() functions. In onNavDestinationSelected() you'd just adapt the NavOptions to your needs.
If you are using navigation component,In addition to this answer From version:'2.4.0-alpha01' it has inbuilt support for multiple back stacks.So no navigation extension is needed
refer to this link for more details. https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f
Try something like this
navView.setOnNavigationItemSelectedListener(onNavigationItemSelectedListener)
And
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.home -> {
fragmentManager.beginTransaction().hide(active).show(homeFragment).commit()
active = homeFragment
return#OnNavigationItemSelectedListener true
}
R.id.news -> {
fragmentManager.beginTransaction().hide(active).show(newsFragment).commit()
active = newsFragment
return#OnNavigationItemSelectedListener true
}
R.id.markets -> {
fragmentManager.beginTransaction().hide(active).show(marketsFragment).commit()
active = marketsFragment
return#OnNavigationItemSelectedListener true
}
R.id.explore -> {
fragmentManager.beginTransaction().hide(active).show(exploreFragment).commit()
active = exploreFragment
return#OnNavigationItemSelectedListener true
}
}
false
}
create a class:
#Navigator.Name("keep_state_fragment") // `keep_state_fragment` is used in navigation xml
class KeepStateNavigator(
private val context: Context,
private val manager: FragmentManager, // Should pass childFragmentManager.
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Navigator.Extras?
): NavDestination? {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
var initialNavigate = false
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
} else {
initialNavigate = true
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
val className = destination.className
fragment = manager.fragmentFactory.instantiate(context.classLoader, className)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commitNow()
return if (initialNavigate) {
destination
} else {
null
}
}
}
Use keep_state_fragment instead of fragment in nav_graph
In Activity:
val navController = findNavController(R.id.nav_host_fragment)
// get fragment
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)!!
// setup custom navigator
val navigator = KeepStateNavigator(this, navHostFragment.childFragmentManager, R.id.nav_host_fragment)
navController.navigatorProvider += navigator
// set navigation graph
navController.setGraph(R.navigation.nav_graph)
bottom_navigation.setupWithNavController(navController)
Use this snippet:
private fun attachFragment(fragmentTag: String) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
supportFragmentManager.findFragmentByTag(fragmentTag)?.let {
if (supportFragmentManager.backStackEntryCount == 0) return
val currentFragmentTag = supportFragmentManager.getBackStackEntryAt(supportFragmentManager.backStackEntryCount - 1).name
(supportFragmentManager.findFragmentByTag(currentFragmentTag) as? FragmentBase)?.let { curFrag ->
fragmentTransaction.hide(curFrag)
}
fragmentTransaction.show(it)
} ?: run {
when (fragmentTag) {
FragmentHome.TAG -> FragmentBase.newInstance<FragmentHome>()
FragmentAccount.TAG -> FragmentBase.newInstance<FragmentAccount>()
else -> null
}?.let {
fragmentTransaction.add(R.id.container, it, fragmentTag)
fragmentTransaction.addToBackStack(fragmentTag)
}
}
fragmentTransaction.commit()
}
You can use this pass the tag of specific fragment that you want to show now, using method attachFragment(FragmentHome.TAG)
Hi friend, it's new solution:
BottomNavigationView navView = findViewById(R.id.nav_view);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);
binding.navView.setOnItemSelectedListener(new NavigationBarView.OnItemSelectedListener() {
#Override
public boolean onNavigationItemSelected(#NonNull MenuItem item) {
if (item.getItemId() != binding.navView.getSelectedItemId())
NavigationUI.onNavDestinationSelected(item, navController);
return true;
}
});
I was looking for the best way to handle this and finally i came out with this simple idea : deactivate the MenuItem currently selected.
This way, you cannot click twice on it and therefore reloading the fragment is prevented.
Don't forget to Enable it back when you go to another fragment through your navHost.
The mechanic is based on the NavHostFragment which receives your BottomNavigationView from within a fragment/activity.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navHostFragment =
childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navView: BottomNavigationView = view.findViewById(R.id.nav_view)
val navController = navHostFragment.navController
//Here you link the NavHostFragment's navController to your
//bottomMenu
navView.setupWithNavController(navController)
//Add a listener monitoring the destination changes
navController.addOnDestinationChangedListener(object : NavController.OnDestinationChangedListener{
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
/* Disable the selected item and re-enable the others */
for( item in navView.menu.iterator()){
item.isEnabled = item.itemId != navView.selectedItemId
}
}
})
}
Hoping it might help
Lenzy
Try this: For ChipNavigationBar
private void Bottom_navigation() {
final Fragment fragment1 = new home_fragment();
final Fragment fragment2 = new bottom_nav1_Bookmark_Fragment();
final Fragment fragment3 = new bottom_nav1_Search_Fragment();
final FragmentManager fm = getSupportFragmentManager();
ChipNavigationBar chipNavigationBar = findViewById(R.id.chipNavigation);
chipNavigationBar.setItemSelected(R.id.home, true);
fm.beginTransaction().add(R.id.frame, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.frame, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.frame,fragment1, "1").commit();
chipNavigationBar.setOnItemSelectedListener(new ChipNavigationBar.OnItemSelectedListener() {
#Override
public void onItemSelected(int i) {
Fragment active = fragment1;
Fragment fragment = null;
switch (i) {
case R.id.home:
fm.beginTransaction().hide(active).show(fragment1).commit();
active = fragment1;
chipNavigationBar.animate().translationY(0);
break;
case R.id.tajbi:
fm.beginTransaction().hide(active).show(fragment2).commit();
active = fragment2;
break;
case R.id.more_App:
fm.beginTransaction().hide(active).show(fragment3).commit();
active = fragment3;
break;
}
}
});
}
You can resolve this issue by using this solution.
First, declare fragments that are used in the bottom navigation view.
val fragment1: Fragment = HomeFragment()
val fragment2: Fragment = ProfileFragment()
val fragment3: Fragment = SettingsFragment()
val fm: FragmentManager = supportFragmentManager
var active = fragment1
Now you need to setup the bottom navigation like this
private fun setUpBottomNavigation() {
fm.beginTransaction().add(R.id.mainHostFragment, fragment3, "3").hide(fragment3).commit();
fm.beginTransaction().add(R.id.mainHostFragment, fragment2, "2").hide(fragment2).commit();
fm.beginTransaction().add(R.id.mainHostFragment,fragment1, "1").commit();
val navigation = findViewById<View>(R.id.bottomNavigationView) as BottomNavigationView
navigation.setOnItemSelectedListener(mOnNavigationItemSelectedListener)
}
private val mOnNavigationItemSelectedListener =
NavigationBarView.OnItemSelectedListener { item ->
when (item.itemId) {
R.id.homeFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment1).commit()
active = fragment1
}
R.id.assessmentListFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment2).commit()
active = fragment2
}
R.id.settingsFragment -> {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
fm.beginTransaction().hide(active).show(fragment3).commit()
active = fragment3
}
}
false
}
I'm using the Navigation Component in android where I have set 6 fragments initially. The problem is when I added a new fragment (ProfileFragment).
When I navigate to this new fragment from the start destination, pressing the native back button does not pop the current fragment off. Instead, it just stays to the fragment I'm in - the back button does nothing.
Here's my navigation.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/dashboard_navigation"
app:startDestination="#id/dashboardFragment"
>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.devssocial.localodge.ui.dashboard.ui.DashboardFragment"
android:label="DashboardFragment"
>
<action
android:id="#+id/action_dashboardFragment_to_newPostFragment"
app:destination="#id/newPostFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_mediaViewer"
app:destination="#id/mediaViewer"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
<action
android:id="#+id/action_dashboardFragment_to_postDetailFragment"
app:destination="#id/postDetailFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
====================== HERE'S THE PROFILE ACTION ====================
<action
android:id="#+id/action_dashboardFragment_to_profileFragment"
app:destination="#id/profileFragment"
app:enterAnim="#anim/slide_in_up"
app:exitAnim="#anim/slide_out_down"
app:popEnterAnim="#anim/slide_in_up"
app:popExitAnim="#anim/slide_out_down"
/>
=====================================================================
</fragment>
<fragment
android:id="#+id/profileFragment"
android:name="com.devssocial.localodge.ui.profile.ui.ProfileFragment"
android:label="fragment_profile"
tools:layout="#layout/fragment_profile"
/>
</navigation>
In the image above, the highlighted arrow (in the left) is the navigation action I'm having troubles with.
In my Fragment code, I'm navigating as follows:
findNavController().navigate(R.id.action_dashboardFragment_to_profileFragment)
The other navigation actions are working as intended. But for some reason, this newly added fragment does not behave as intended.
There are no logs showing when I navigate to ProfileFragment and when I press the back button.
Am I missing something? or is there anything wrong with my action/fragment configurations?
EDIT:
I do not do anything in ProfileFragment. Here's the code for it:
class ProfileFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_profile, container, false)
}
}
And my activity xml containing the nav host:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/dashboard_navigation"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:navGraph="#navigation/dashboard_navigation"
app:defaultNavHost="true"/>
</FrameLayout>
if you are using setupActionBarWithNavController in Navigation Component such as:
setupActionBarWithNavController(findNavController(R.id.fragment))
then also override and config this methods in your main activity:
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
My MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupActionBarWithNavController(findNavController(R.id.fragment))
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
For anyone using LiveData in a previous Fragment which is a Home Fragment, whenever you go back to the previous Fragment by pressing back button the Fragment is starting to observe the data and because ViewModel survives this operation it immediately emits the last emitted value which in my case opens the Fragment from which I pressed the back button, that way it looks like the back button is not working the solution for this is using something that emits data only once. I used this :
class SingleLiveData<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean()
/**
* Adds the given observer to the observers list within the lifespan of the given
* owner. The events are dispatched on the main thread. If LiveData already has data
* set, it will be delivered to the observer.
*
* #param owner The LifecycleOwner which controls the observer
* #param observer The observer that will receive the events
* #see MutableLiveData.observe
*/
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
/**
* Sets the value. If there are active observers, the value will be dispatched to them.
*
* #param value The new value
* #see MutableLiveData.setValue
*/
#MainThread
override fun setValue(value: T?) {
pending.set(true)
super.setValue(value)
}
This problem happened to me while using MutableLiveData to navigate between fragments and was observing the live data object at more than one fragment.
I solved it by observing the live data object one time only or by using SingleLiveEvent instead of MutableLiveData. So If you're having the same scenario here, try to observe the live data object one time only or use SingleLiveEvent.
You can use this following for the Activity
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
For the fragment, It will be needed requireActivity() along with Callback
requireActivity().onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
requireActivity().onBackPressed()
// if you want onBackPressed() to be called as normal afterwards
}
}
)
If you have a Button or something else to perform an action then you can use
this.findNavController().popBackStack()
You need to set the MutableLiveData to null once the navigation is done.
For example
private val _name = MutableLiveData<String>()
val name: LiveData<String>
get() = _name
fun printName(){
_name.value = "John"
}
fun navigationComplete(){
_name.value = null
}
Now say you are observing the "name" in your fragment and you are doing some navigation once the name is John then should be like that:
viewModel.name.observe(viewLifecycleOwner, Observer { name ->
when (name) {
"John" -> {
this.findNavController() .navigate(BlaBlaFragmentDirections.actionBlaBlaFragmentToBlaBlaFragment())
viewModel.navigationComplete()
}
}
})
Now your back button will be working without a single problem.
Some data are almost used only once, like a Snackbar message or navigation event therefore you must tell set the value to null once done used.
The problem is that the value in _name remains true and it’s not possible to go back to previous fragment.
If you use Moxy or similar libs, checkout the strategy when you navigate from one fragment to second.
I had the same issue when strategy was AddToEndSingleStrategy.
You need change it to SkipStrategy.
interface ZonesListView : MvpView {
#StateStrategyType(SkipStrategy::class)
fun navigateToChannelsList(zoneId: String, zoneName: String)
}
Call onBackPressed in OnCreateView
private fun onBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(this) {
//Do something
}
}
For everyone who is using LiveData for setting navigation ids, there's no need to use SingleLiveEvent. You can just set the destinationId as null after you set its initial value.
For instance if you want to navigate from Fragment A to B.
ViewModel A:
val destinationId = MutableLiveData<Int>()
fun onNavigateToFragmentB(){
destinationId.value = R.id.fragmentB
destinationId.value = null
}
This will still trigger the Observer in the Fragment and will do the navigation.
Fragment A
viewModel.destinationId.observe(viewLifecycleOwner, { destinationId ->
when (destinationId) {
R.id.fragmentB -> navigateTo(destinationId)
}
})
The Simplest Answer for your problem (If it has something to do with fragments - Bottom navigation) could be
To set defaultNavHost = "false"
From Official Documentation it says->
Let's say you have 3 fragments set for Bottom Navigation, then setting
"defaultNavHost = true" will make fragment A acts like a parent, so when user clicks on back button in fragment 3 , it comes to fragment 1 instead of closing the activity (Bottom Navigation as Example).
Your XML should look like this, if you wanna just press back and close the activity from any fragment you are in.
<fragment
android:id="#+id/fragmentContainerView"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#+id/bottom_nav"
app:defaultNavHost="false"
app:navGraph="#navigation/visit_summary_navigation" />
Set the MutableLiveData to false after navigation
Put this code in your ViewModel.kt
private val _eventNextFragment = MutableLiveData<Boolean>()
val eventNextFragment: LiveData<Boolean>
get() = _eventNextFragment
fun onNextFragment() {
_eventNextFragment.value = true
}
fun onNextFragmentComplete(){
_eventNextFragment.value = false
}
Let's say you want to navigate to another fragment, you'll call the onNextFragmentComplete method from the viewModel immediately after navigating action.
Put this code in your Fragment.kt
private fun nextFragment() {
val action = actionFirstFragmentToSecondFragment()
NavHostFragment.findNavController(this).navigate(action)
viewModel.onNextFragmentComplete()
}
I had faced the same issue due to the below "run blocking" code block. So don't use it if not necessary.
What I have done:
I have created Navigation Drawer Activity, As updated new format of Navigation Drawer Activity, As per new Android architecture, I got it with Navigation Component structure.
The NavigationView code with NavController and NavigationUI is below which is opening fragment when I click on any navigation item.
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navigationView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_profile, R.id.nav_privacy_policy,
R.id.nav_terms, R.id.nav_contact_us, R.id.nav_share, R.id.nav_send)
.setDrawerLayout(drawer)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(navigationView, navController);
This is for nav_host_fragment:
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/mobile_navigation" />
The navigation is happening using this navigation/mobile_navigation.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/mobile_navigation"
app:startDestination="#+id/nav_home">
<fragment
android:id="#+id/nav_home"
android:name="com.sohamerp.marsremedies.fragment.HomeFragment"
android:label="#string/menu_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/nav_profile"
android:name="com.sohamerp.marsremedies.fragment.ProfileFragment"
android:label="#string/menu_my_profile"
tools:layout="#layout/fragment_profile" />
<fragment
android:id="#+id/nav_privacy_policy"
android:name="com.sohamerp.marsremedies.fragment.PrivacyPolicyFragment"
android:label="#string/menu_privacy_policy"
tools:layout="#layout/fragment_privacy_policy" />
<fragment
android:id="#+id/nav_terms"
android:name="com.sohamerp.marsremedies.fragment.TermsConditionFragment"
android:label="#string/menu_terms"
tools:layout="#layout/fragment_terms_condition" />
<fragment
android:id="#+id/nav_contact_us"
android:name="com.sohamerp.marsremedies.fragment.ContactUsFragment"
android:label="#string/menu_contact_us"
tools:layout="#layout/fragment_terms_condition" />
</navigation>
What I want to do:
Now I want to pass some values as a bundle (arguments) in Fragment when it's called.
Scenario: I have two fragments PrivacyPolicyFragment and TermsConditionsFragment, In both fragments, I am just opening links inside WebView accordingly. So When I click on the menu item of Privacy Policy, I will pass a link related to the same.
In this new structure navigation/mobile_navigation.xml opening fragments, How can I pass arguments?
Any help?
So I forgot to go through this link : Define Destination Arguments
But this answer helpful to all lazy peoples like me:
Add dependency in project level build.gradle:
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0"
Apply plugin in app level build.gradle:
apply plugin: "androidx.navigation.safeargs"
Using XML: predefined (static) value:
In xml file of navigation /navigation/mobile_navigation.xml declare argument tag as below or you can design through this link:
<fragment
android:id="#+id/nav_privacy_policy"
android:name="com.sohamerp.marsremedies.fragment.PrivacyPolicyFragment"
android:label="#string/menu_privacy_policy"
tools:layout="#layout/fragment_privacy_policy" >
<argument
android:name="privacyPolicyLink"
app:argType="string"
android:defaultValue="http://sohamerp.com/avo/avo_privacy_policy.html"/>
</fragment>
<fragment
android:id="#+id/nav_terms"
android:name="com.sohamerp.marsremedies.fragment.PrivacyPolicyFragment"
android:label="#string/menu_terms"
tools:layout="#layout/fragment_terms_condition" >
<argument
android:name="privacyPolicyLink"
app:argType="string"
android:defaultValue="http://sohamerp.com/avo/avo_privacy_policy.html"/>
</fragment>
Now you have to write code in your Fragment like:
if(getArguments() != null) {
// The getPrivacyPolicyLink() method will be created automatically.
String url = PrivacyPolicyFragmentArgs.fromBundle(getArguments()).getPrivacyPolicyLink();
}
Hope it will helps you others.
Simple and fast solution:
pass arguments between destinations
Bundle bundle = new Bundle();
bundle.putString("amount", amount);
Navigation.findNavController(view).navigate(R.id.confirmationAction, bundle);
and receiving
TextView tv = view.findViewById(R.id.textViewAmount);
tv.setText(getArguments().getString("amount"));
In this scenario, you can use
private NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
// Create the Bundle to pass, you can put String, Integer, or serializable object
Bundle bundle = new Bundle();
bundle.putString("link","http://yourlink.com/policy");
bundle.putSerializable("USER", user); // Serializable Object
navController.navigate(R.id.nav_terms, bundle); // called fragment with agruments
In case of any help you can reply on it
To pass arguments to other Fragments/Destinations, use Safe Args which ensures type safety. Just like #bromden illustrated, Safe Args will generate a class for each fragment/destination where an action originates. You can then pass the arguments into the action that navigates to the Fragments.
In the receiving fragment, say PrivacyFragment if your code is in Kotlin, use by navArgs() property delegate to access the arguments. i.e.
val args: PrivacyFragmentArgs by navArgs()
To better understand this, visit Pass data between destinations
In newer version of Android Studio 3.2+, below dependency and plug-in need to add in both build.gradle file
Step-1
Add dependency in Project-Level build.gradle
dependencies {
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5'
}
Apply plugins in App-Level build.gradle
plugins {
id 'androidx.navigation.safeargs'
}
Step-2
In Navigation file, res/navigation/nav_graph.xml
Declare argument tag in any fragment or inner fragment with action tag
List item
Sample xml code
<fragment
android:id="#+id/nav_register"
android:name="com.pd.demo.ui.profile.RegisterFragment"
android:label="#string/title_register"
tools:layout="#layout/fragment_register">
<action
android:id="#+id/action_nav_register_to_nav_verify_otp"
app:destination="#id/nav_verify_otp">
<argument
android:name="mobile"
app:argType="string" />
<argument
android:name="password"
app:argType="string" />
</action>
</fragment>
Step-3
Below Kotlin code, pass argument to destination fragment
val bundle = bundleOf("mobile" to binding.etMobileNo.text.toString().trim())
Navigation.findNavController(binding.root).navigate(R.id.action_nav_register_to_nav_verify_otp, bundle)
Step-4
Below Kotlin code, get bundle argument from source fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
mobileNo = arguments!!.getString("mobile").toString()
password = arguments!!.getString("password").toString()
}
This code will helps
You could implement NavigationView.OnNavigationItemSelectedListener
And do something like this:
override fun onNavigationItemSelected(item: MenuItem): Boolean {
drawer_layout.closeDrawers()
if (item.itemId == nv_navigation_drawer_navigation_view.checkedItem?.itemId)
return false
Handler().postDelayed({
when (item.itemId) {
R.id.nav_privacy_policy -> {
val action = FragmentDirections.actionFragmentToPrivacyFragment("Policy link")
findNavController().navigate(action)
}
}
}, DRAWER_NAVIGATION_DELAY)
return true
}
And in xml you can add argument to the recieving fragment, in this case
<fragment
android:id="#+id/nav_privacy_policy"
android:name=".fragment.PrivacyPolicyFragment"
android:label="#string/menu_privacy_policy"
tools:layout="#layout/fragment_privacy_policy">
<argument
android:name="policy"
app:argType="string" />
</fragment>
You can also pass serializable objects, enum values and arrays of primitive types.
For example:
enum class ObjectType : Serializable {
FIRST, SECOND
}
Then, add arguments to the xml
<fragment
android:id="#+id/nav_profile"
android:name="com.sohamerp.marsremedies.fragment.ProfileFragment"
android:label="#string/menu_my_profile"
tools:layout="#layout/fragment_profile" >
<argument
android:name="myObjectType"
android:defaultValue="SECOND"
app:argType="com.project.app.data.ObjectType" />
</fragment>
Note, that you should specify complete path!
Passing data from the start destination with NavController NavGraph navigate is straightforward. I use this to display order lines associated to an order header:
private void showRepositionLinesFragment(AppObjects.RepOrderHeader orderHeader) {
int number = orderHeader.getOrderNumber();
String orderNumber = String.format("%06d",number);
String createDate = orderHeader.getCreateDate();
Globals.LogTrace(this, AppAlertDialog.DialogType.Info,
"Navigate to FragRepoLines with orderNumber: " + orderNumber,false);
NavController navController = NavHostFragment.findNavController(FragmentRepositionHeaders.this);
Bundle bundle = new Bundle();
bundle.putString(getString(R.string.arg_header_ordernumber),orderNumber);
bundle.putString(getString(R.string.arg_repheader_createdate), createDate);
navController.getGraph().findNode(R.id.FragRepoLines).setLabel(orderNumber + " " + createDate);
navController.navigate(R.id.action_FragRepoHeaders_to_FragRepoLines,bundle);
}
Getting data from the fragment that handles the order lines turned to be more complicated. Tried for hours with NavController getArguments().
In the end this is what worked for me.
In the start fragment:
NavController navController = NavHostFragment.findNavController(this);
// We use a String here, but any type that can be put in a Bundle is supported
MutableLiveData<String> liveData = navController.getCurrentBackStackEntry()
.getSavedStateHandle()
.getLiveData(getString(R.string.arg_header_ordernumber));
liveData.observe(getViewLifecycleOwner(), new Observer<String>() {
#Override
public void onChanged(String s) {
Globals.LogTrace(this, AppAlertDialog.DialogType.Info, "+++++++++ liveData changed -> " + s, false);
}
});
In the destination fragment:
String arg = getString(R.string.arg_header_ordernumber);
NavController navController = NavHostFragment.findNavController(this);
NavBackStackEntry navBackStackEntry = navController.getCurrentBackStackEntry();
if (navBackStackEntry != null) {
SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
if (savedStateHandle != null) {
savedStateHandle.set(arg, "000000");
} else {
Globals.LogTrace(this, AppAlertDialog.DialogType.Info,"savedStateHandle == null",false);
}
} else {
Globals.LogTrace(this, AppAlertDialog.DialogType.Info,"navBackStackEntry == null",false);
}
Source: Interact programmatically with the Navigation component
I changed the navController.getPreviousBackStackEntry() for navController.getCurrentBackStackEntry()
I had the same issue but I´m still not able to pass the arguments using fragment directions. Since I need the value in several of my fragments I decided to use a companion object in my main activity. It´s probably not the best but it solves the problem:
class MainActivity : AppCompatActivity() {
companion object{
var myGlobalVar = "Example"
}
override fun onCreate(savedInstanceState: Bundle?) {....
Then I can access its value in all of my fragments by importing it:
import myAppPackage.MainActivity.Companion.myGlobalVar
I had to delete the argument from my navGraph but i can still access it in the background.