I am using 3 fragments with 1 activity for user profile features. I use navigation to move to fragments:
The problem is when I go from profileFragment to editProfileFragment, change user's data and then go back to profileFragment by navigation action, I cannot reach editProfileFragment again from profileFragment. I got this error:
Navigation action/destination xxx:id/action_profileFragment_to_editProfileFragment cannot be found from the current destination Destination(xxx:id/editProfileFragment)
I am trying to use MVVM architecture so my navigation goes like this - in fragment I observe LiveData:
navController = Navigation.findNavController(view)
viewModel.navigateTo.observe(viewLifecycleOwner, EventObserver {
navController.navigate(it)
})
viewModel 'navigateTo':
private val _navigateTo = MutableLiveData<Event<Int>>()
val navigateTo: LiveData<Event<Int>> = _navigateTo
and the navigation methods:
fun goBackToProfile(){
_navigateTo.value = Event(R.id.action_editProfileFragment_to_profileFragment)
}
fun editProfileButtonClick() {
_navigateTo.value = Event(R.id.action_profileFragment_to_editProfileFragment)
}
I also use Event wrapper class by Jose Alcerreca:
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let {
onEventUnhandledContent(it)
}
}
}
I don't know if error occurs because of Event wrapper or I do something wrong with navigation. I will appreciate any advices.
EDIT:
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/main_nav_graph"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="xxx.mainPackage.views.HomeFragment"
android:label="fragment_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/profileFragment"
android:name="xxx.mainPackage.views.ProfileFragment"
android:label="fragment_profile"
tools:layout="#layout/fragment_profile" >
<action
android:id="#+id/action_profileFragment_to_editProfileFragment"
app:destination="#id/editProfileFragment" />
</fragment>
<fragment
android:id="#+id/editProfileFragment"
android:name="xxx.mainPackage.views.EditProfileFragment"
android:label="fragment_edit_profile"
tools:layout="#layout/fragment_edit_profile" >
<action
android:id="#+id/action_editProfileFragment_to_chooseImageFragment"
app:destination="#id/chooseImageFragment" />
<action
android:id="#+id/action_editProfileFragment_to_profileFragment"
app:destination="#id/profileFragment" />
</fragment>
<fragment
android:id="#+id/chooseImageFragment"
android:name="xxx.mainPackage.views.ChooseImageFragment"
android:label="ChooseImageFragment" >
<action
android:id="#+id/action_chooseImageFragment_to_editProfileFragment"
app:destination="#id/editProfileFragment" />
</fragment>
</navigation>
It looks to me like you're navigating the wrong way.
Firstly
You are moving from ProfileFragment to EditProfileFragment. Then you move from EditProfileFragment to ProfileFragment via action whose id is R.id.action_profileFragment_to_editProfileFragment.
That action I see you define from Fragment ProfileFragment, not EditFragment.
Make sure your LiveData navigateTo in EditProfileFragment trigger id is R.id.action_editProfileFragment_to_profileFragment when you want to open ProfileFragment from EditProfileFragment.
Secondly
When you return from EditProfileFragment , don't call navController.navigate() as it will keep your current fragment in the backstack and push your target fragment to the backstack. If you want to go back without keeping your current fragment in the backstack, call navController.popBackStack().
Error is very clear, there is no action_profileFragment_to_editProfileFragment defined on editProfileFragment. if you look at your navigation xml, you can see that action_profileFragment_to_editProfileFragment is defined for ProfileFragment.
But you are trying to use this navigation action from EditProfileFragment, which causes the error. so either update your navigation to include this action for EditProfileFragment or use some action which is already defined for EditProfileFragment.
I have a BottomNavigationView and I am using a NavController to switch menus and move the screen.
I would like to know how to keep a specific screen even when moving the menu freely.
Let me explain in more detail.
There are bottom menus A, B, C. And each has a Fragment screen.
All menus and screen transitions use NavHost, NavController, and nav_graph.
In menu C, a dialog is displayed through the button on the C screen.
The dialog is also linked to the nav_graph.
When an option is selected in the dialog, it moves to screen D.
D screen is the page where you write something.
This is important from now on.
The current menu is C, screen D is open, and something is being written.
However, if you go to another menu while writing and then return to C, screen C of the first menu C appears, not the screen you are writing.
Here, I want the screen I was writing to continue to be displayed even when I return from another menu.
Any good way?
For reference, since I am using the mvvm pattern and viewmodel, the data of the screen I am writing seems to be maintained.
Thank you.
nav_graph
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/calendar">
<!-- menu C screen -->
<fragment
android:id="#+id/write_home"
android:name="com.example.writeweight.fragment.WriteRoutineHomeFragment"
android:label="fragment_write_routine_home"
tools:layout="#layout/fragment_write_routine_home" >
<action
android:id="#+id/action_write_home_to_bodyPartDialog"
app:destination="#id/bodyPartDialog" />
</fragment>
<!-- dialog -->
<dialog
android:id="#+id/bodyPartDialog"
android:name="com.example.writeweight.fragment.BodyPartDialogFragment"
android:label="BodyPartDialogFragment"
tools:layout="#layout/fragment_body_part_dialog">
<action
android:id="#+id/action_bodyPartDialog_to_write"
app:destination="#id/write"/>
</dialog>
<!-- screen D (write page) -->
<fragment
android:id="#+id/write"
android:name="com.example.writeweight.fragment.WriteRoutineFragment"
android:label="WritingRoutineFragment"
tools:layout="#layout/fragment_writing_routine">
<action
android:id="#+id/action_write_to_workoutListTabFragment"
app:destination="#id/workoutListTabFragment" />
<argument
android:name="title"
app:argType="string"
app:nullable="true"
android:defaultValue="#null"/>
<argument
android:name="workout"
app:argType="string"
app:nullable="true"
android:defaultValue="#null"/>
</fragment>
</navigation>
There are two ways to fix this issue either use version_navigation 2.4.0-alpha01 which is the easiest way or use NavigationExtensions
to use NavigationExtensions you have to add this to your project NavigationExtensions and then in the navigation menu create separate navigation files for each tab.
In the main activity, layout replace fragment with FragmentContainerView and remove NavHostFragment.
In the Mainactivty
class MainActivity : AppCompatActivity(), NavController.OnDestinationChangedListener {
private val viewModel by viewModels<MainViewModel>()
private var currentNavController: LiveData<NavController>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
setSupportActionBar(toolbar)
/*
appBarConfiguration = AppBarConfiguration(
// navController.graph,
setOf(
R.id.navigate_home, R.id.navigate_collection, R.id.navigate_profile
),
drawerLayout
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
bottomNavView.setupWithNavController(navController)
// make sure appbar/toolbar is not hidden upon fragment switch
navController.addOnDestinationChangedListener { controller, destination, arguments ->
if (destination.id in bottomNavDestinationIds) {
appBarLayout.setExpanded(true, true)
}
}
*/
// Add your tab fragments
val navGraphIds = listOf(R.navigation.home, R.navigation.albumlist, R.navigation.test)
val controller = bottomNavView.setupWithNavController(
navGraphIds = navGraphIds,
fragmentManager = supportFragmentManager,
containerId = R.id.nav_host_container,
intent = intent
)
// Whenever the selected controller changes, setup the action bar.
controller.observe(this, Observer { navController ->
setupActionBarWithNavController(navController)
// optional NavigationView for Drawer implementation
// navView.setupWithNavController(navController)
addOnDestinationChangedListener(navController)
})
currentNavController = controller
}
private fun addOnDestinationChangedListener(navController: NavController) {
// ensure only one listener is active
navController.removeOnDestinationChangedListener(this)
navController.addOnDestinationChangedListener(this)
}
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
if (destination.id in bottomNavDestinationIds) {
appBarLayout.setExpanded(true, true)
}
}}
The context:
I want to pass a userId to a fragment UserFragment after he has logged in.
I use a new project Android type "Navigation drawer activity".
I use LiveData, so when the user is logged in, the observer of my livedata updates the idUser from MainActivity. I would also like to update the fragment argument in my Navigation Component...
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navView: NavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(setOf(
R.id.nav_home,
R.id.userDetailItemNav,
R.id.dashboardItemNav), drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
My problem:
I juste want to pass the value of userId as argument for the destination (the UserDetails fragment) when navigating... But I don't want to create an onClickListner and pass the argument in the bundle. I only want to use the Navigation component solution and therefore just change the value of the argument of the fragment declared in my Navigation Component
I added to my project the "Safe Args".
I added on one of the destination fragments a "theUserId" argument:
In mobile_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/dashboardItemNav">
<fragment (...) />
<fragment (...) />
<fragment
android:id="#+id/userDetailItemNav"
android:name="com.example.smart.ui.user.UserFragment"
tools:layout="#layout/fragment_user" >
<argument
android:name="theUserId"
app:argType="long"
android:defaultValue="-1L"
/>
</fragment>
</navigation>
In UserFragment.kt:
// loading specifics arguments
val argument = arguments?.getLong("theUserId",999L)
Timber.i("userId from MainActivity: $argument")
What i tried:
My LiveData observer in onCreate() in MainActivity.kt is:
viewModel.user.observe(this, Observer { user ->
if (user != null) {
userId = user.id
// TODO: here, change the value of my navigation argument theUserId
}
I tried to adding an addOnDestinationChangedListner in my LiveData Observer (but it multiplies the Listner). I tried also to put after the observer, directly in the OnCreate of my MainActivity (seems better) :
navController.addOnDestinationChangedListener {_, destination, arguments ->
if (destination.id == R.id.userDetailItemNav) {
userId?.let {
val argument = NavArgument.Builder()
.setDefaultValue(it)
.build()
}
}
}
...but the userId value is updated only after the second click on the navigation item of my UserDetail fragment. I don't understand why.
I think this code is better to put in my Livedata observer:
var userId = 75 // 75 is from my Livedata for example
var myArgs = UserFragmentArgs.Builder().setTheUserId(userId).build()
... but I don't know what to do with myArgs... how to use it, how to inject it into the Navigation Component in order to the next time I click on the drawer item "Detail User", my argument "theUserId" is well modified and retrieved in the fragment UserFragment.
Thank you for your help.
It is not clean, but here is a solution that works. Unfortunately, this solution does not fully respect the spirit of the Navigation Component...
In MainActivity.kt, in the onCreate ( ) function :
viewModel.user.observe(this, Observer { user ->
if (user != null) {
userId = user.id
}
}
// (...)
navView.setNavigationItemSelectedListener { menuItem ->
drawerLayout.closeDrawers() // close drawer when item is tapped
val bundle = bundleOf("theUserId" to userId)
when (menuItem.itemId) {
R.id.userDetailItemNav -> {
navController.navigate(menuItem.itemId, bundle)
}
else -> { // for other destination without parameter
navController.navigate(menuItem.itemId)
}
}
true
}
If you ever have a better idea (i.e. being able to change the value of theUserId parameter directly in the observer), I would take your advice with great pleasure !.
I believe the example from Documentation is easily adapted for your use case.
I have a single activity which has a bottom navigation view in order to open different fragments.
The top level/default fragments loads data from firebase and then i want to pass that data to different fragments when user switches to different fragment.
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/navigation"
app:startDestination="#id/homeFragment">
<fragment
android:id="#+id/homeFragment"
android:name="com.ankitrath.finderr.HomeFragment"
android:label="fragment_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/searchFragment"
android:name="com.ankitrath.finderr.SearchFragment"
android:label="fragment_search"
tools:layout="#layout/fragment_search" />
<fragment
android:id="#+id/requestFragment"
android:name="com.ankitrath.finderr.RequestFragment"
android:label="fragment_request"
tools:layout="#layout/fragment_request" />
<fragment
android:id="#+id/friendFragment"
android:name="com.ankitrath.finderr.FriendFragment"
android:label="fragment_friend"
tools:layout="#layout/fragment_friendd" />
</navigation>
My MainActivity's OnCreate has:
BottomNavigationView bottomNavigationView = findViewById(R.id.bottomNavigationView);
NavController navController = Navigation.findNavController(this, R.id.fragment);
NavigationUI.setupWithNavController(bottomNavigationView, navController);
When HomeFragment loads the data from firestore. I want to pass 2 values to the other fragments.
I did look up the docs but I couldn't understand.
The easiest solution for this is to just use Viewmodel instead of passing it between activity and the fragment. Then u pass the activity on the ViewModel so every fragment and the parent have the same ViewModel.
This is an example of how to do it val viewModel by activityViewModels<The ViewModel that u made>()
you can use VIEWHOLDER to share data between fragments or activities, here is an example
in your app gradle add this implementation
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
Create a viewHolder Class like that
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SharedDataViewModel: ViewModel() {
private val _selectedCycle = MutableLiveData<Cycle>()
val selectedCycle: LiveData<Cycle> = _selectedCycle
private val _isAdmin = MutableLiveData<Boolean>()
val isAdmin: LiveData<Boolean> = _isAdmin
fun getSelectedCycle():Cycle {
return _selectedCycle.value!!
}
fun setSelectedCycle(cycle: Cycle) {
_selectedCycle.value = cycle
}
fun getIsAdmin():Boolean {
return _isAdmin.value!!
}
fun setIsAdmin(value: Boolean) {
_isAdmin.value = value
}
}
and use it like that in every fragment or activity
private val sharedData: SharedDataViewModel by activityViewModels()
then set or get the new values
sharedData.getIsAdmin()
sharedData.setIsAdmin(true)
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(...)