58

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?

Rajarshi
  • 2,419
  • 3
  • 23
  • 36
  • 3
    possibly duplicated of https://stackoverflow.com/questions/50599238/dynamic-actionbar-title-from-a-fragment-using-androidx-navigation/53830472 – Agna JirKon Rx Jul 25 '19 at 22:46
  • Does this answer your question? [Dynamic ActionBar title from a Fragment using AndroidX Navigation](https://stackoverflow.com/questions/50599238/dynamic-actionbar-title-from-a-fragment-using-androidx-navigation) – AdamHurwitz Dec 14 '20 at 18:15

19 Answers19

58

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

ALXKAY
  • 21
  • 1
  • 7
ʍѳђઽ૯ท
  • 16,646
  • 7
  • 53
  • 108
  • I have already tried this and the outcome is if I don't use `Navigation.findNavController(view).getCurrentDestination().setLabel("Hello")` in the `MainFragment` then the title will remain "Detail". And don't know why, someone down voted the question :p. – Rajarshi Sep 26 '18 at 10:20
  • I want to be clear here, adding `setTitle` along with `Navigation.findNavController(view).getCurrentDestination().setLabel("Hello")` seems to be the only way to get the title, even if I start the app or press the back button. So aren't there any viable way with a single line of code? Ok, let me update the question. – Rajarshi Sep 26 '18 at 10:32
  • 3
    So, there's is problem with Navigation component. – Rajarshi Sep 26 '18 at 11:10
  • Manually editing the title bar, or assuming the contents of `getCurrentDestination` will get it things of sync with the NavController. The correct solution is at https://stackoverflow.com/questions/50599238/dynamic-actionbar-title-from-a-fragment-using-androidx-navigation/54504353#54504353 – gladed Dec 31 '19 at 01:54
37

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"        
//    }
}
FlashspeedIfe
  • 368
  • 1
  • 6
  • 15
Rajarshi
  • 2,419
  • 3
  • 23
  • 36
  • 1
    In case you want no title at all, you cannot provide `null` or an empty string. Instead you have to set the `destination.label` to `" "`. I added this as an example in your code (should be visible after peer review of my changes) – muetzenflo Aug 16 '19 at 15:00
  • 1
    @muetzenflo this has been fixed starting on Navigation UI "2.3.0-alpha04" – Ally May 02 '20 at 12:42
  • 1
    Or you can set directly destination.label and the appBar label changes. The `OnDestinationChangedListener` has to be set before calling `NavigationUI.setupActionBarWithNavController(...)`. – Vít Kapitola Jul 22 '20 at 09:47
  • 2
    `(requireActivity() as MainActivity).title` does not change the label, even after removing the label from the Navigation Graph. Perhaps the API functionality has recently changed. – AdamHurwitz Dec 13 '20 at 02:29
  • Or just add labels to all fragments in the navigation app graph, and set `title = destination.label`, as in: `findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener { controller, destination, arguments -> title = destination.label }` – MiStr Feb 13 '21 at 17:04
23

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 ^-^

Alif Al-Gibran
  • 1,166
  • 1
  • 13
  • 24
  • The key for me was to **delete** the `android:label` **attribute** from my Fragment in the navigation xml file. Then I was able to dynamically change the title of my Fragment by using `toolbar?.title = "my new name"`. – MatJB May 04 '23 at 13:17
15

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"
        }

    }
Gstuntz
  • 434
  • 4
  • 10
  • 4
    for anyone else being as stupid as me and wondering why it is not working: you have to add `supportActionBar?.title = title` at the bottom to apply the title to the actionbar – Max90 Apr 03 '20 at 05:27
  • You can also set title with `destination.setLabel()` but it has to be before calling the ` NavigationUI.setupActionBarWithNavController(...); method. ` – Vít Kapitola Jul 22 '20 at 08:53
14

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.

fraggjkee
  • 3,524
  • 2
  • 32
  • 38
9

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

5
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)
}
Sumit Shukla
  • 4,116
  • 5
  • 38
  • 57
5

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);

}

loonighan
  • 431
  • 6
  • 9
2

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.

enter image description here

This is pretty helpful if you want a static title for your fragments.

malrok44
  • 592
  • 6
  • 17
  • 1
    what if want to reuse this fragment and need to change the label? How will I do it programatically – Shikhar May 07 '20 at 10:40
2

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

Reza
  • 845
  • 13
  • 18
2

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>
陳遠謀
  • 19
  • 3
1

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.

polina-c
  • 6,245
  • 5
  • 25
  • 36
1

Another approach could be this:

fun Fragment.setToolbarTitle(title: String) {
    (activity as NavigationDrawerActivity).supportActionBar?.title = title
}
drindt
  • 901
  • 10
  • 15
1

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
    })
Stuart Campbell
  • 1,141
  • 11
  • 13
0

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")
}
0

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)
Roman
  • 2,464
  • 2
  • 17
  • 21
0

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
    }

}

avvensis
  • 917
  • 10
  • 20
0

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(...)
Yaqoob Bhatti
  • 1,271
  • 3
  • 14
  • 30
0

My own simple solution at last date library. Before calling navigate do as:

findNavController().findDestination(R.id.yourNavFragmentId)?.label = "Your title"

then navigate to your fragment

findNavController().navigate(R.id.yourNavFragmentId)