androidkotlinandroid-fragmentsandroid-activityandroid-toolbar

How to apply different toolbar(actionbar) layout for each fragment?


I am applying Single Activity Architecture, and I would like to use the top fixing bar using the toolbar.

Within my project, the form of the toolbar varies from fragment to fragment, and the form is as follows:

Main(Home)

a few(1 or 2) fragment with drawer

other fragment

In the MainFragment and one or two Fragments, the toolbar has a drawerLayout and two menu option, and in all Fragments except for that, it has a back button and no menu option.

But I don't know how to change toolbar's layout for each fragment.

I have two ideas for changing toolbar layout. The first way is to use the toolbar for Activity xml and change the menu on the code when I move the fragment using menuProvider. Like this:

MainActivity.kt


    private fun initToolbar() {
        setSupportActionBar(binding.content.toolbar)
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
        supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_action_open_drawer)
        supportActionBar?.setDisplayShowTitleEnabled(false)
        binding.content.toolbar.title = "test"
    }

    private fun initMenuProvider() {
        addMenuProvider(object : MenuProvider {
            override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
                menuInflater.inflate(R.menu.toolbar_activity, menu)
            }

            override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
                when (menuItem.itemId) {
                    android.R.id.home -> { // for open drawerLayout
                        binding.drawerLayout.openDrawer(GravityCompat.START)
                    }
                }
                return true
            }
        })
    }

    fun changeToolbar(menuProvider: MenuProvider, title: String) {
        supportActionBar?.setHomeAsUpIndicator(null)
        addMenuProvider(menuProvider)
        binding.content.toolbar.title = title
    }

OtherFragment.kt


    override fun onViewCreated() {
        super.onViewCreated(view, savedInstanceState)
        val menuProvider = object : MenuProvider { 
            override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
                menuInflater.inflate(R.menu.toolbar_nothing, menu) // empty menu layout
            }

            override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
                return true
            }
        }

        requireActivity().changeToolbar(menuProvider!!, "Other Fragment")
        requireActivity().invalidateOptionsMenu()
    }

This code succeeded in changing the dehaze mark to back button and fixing the toolbar's title to fragment title. However, there are still two menu options left, and the openDrawer() runs even when the go back button is pressed. And even if this way works properly, I will have to run the changeToolbar() of activity for every fragment's onViewCreated.

Another way is not to manage toolbars in activity, but to add toolbars to xml for each fragment. But I think this is going to be a very inefficient operation.

What method should I use?


Solution

  • I solved it in the following way.

    Never use setSupportActionBar()

    In my project, the following conditions exist:

    1. The toolbar layout should be shared in a total of three fragments: Main Fragment, Fragment A, and B. BackButton should be shown in the remaining fragments.
    2. In Main Fragment, the home button must open the Navigation View (Drawer), and in Fragment with Back Button, it must move to Fragment before moving. (popBackStack()).
    3. In Main Fragment, the title should be the logo of the application, and in Fragments A and B, the name of the fragment.

    The solution was as follows:

    1. Add each Toolbar Layout to MainActivity. (It doesn't matter if you want to create a toolbar_layout.xml file and include the layout)

      <androidx.constraintlayout.widget.ConstraintLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       tools:context=".ui.MainActivity">
      
           <androidx.appcompat.widget.Toolbar
               android:id="@+id/default_toolbar"
               android:layout_width="match_parent"
               android:layout_height="?attr/actionBarSize"
               android:background="?attr/colorPrimary"
               app:layout_constraintTop_toTopOf="parent">
      
           </androidx.appcompat.widget.Toolbar>
      
           <androidx.appcompat.widget.Toolbar
               android:id="@+id/backstack_toolbar"
               android:layout_width="match_parent"
               android:layout_height="?attr/actionBarSize"
               android:background="?attr/colorPrimary"
               app:layout_constraintTop_toTopOf="parent"
               android:visibility="gone">
      
           </androidx.appcompat.widget.Toolbar>
      
           <androidx.fragment.app.FragmentContainerView
               android:id="@+id/nav_host"
               android:name="androidx.navigation.fragment.NavHostFragment"
               android:layout_width="match_parent"
               android:layout_height="0dp"
               app:navGraph="@navigation/nav_graph"
               app:defaultNavHost="true"
               android:layout_marginTop="?attr/actionBarSize"
               app:layout_constraintStart_toStartOf="parent"
               app:layout_constraintEnd_toEndOf="parent"
               app:layout_constraintBottom_toBottomOf="parent"
               app:layout_constraintTop_toTopOf="parent"/>
           ...
       </androidx.constraintlayout.widget.ConstraintLayout>
      

    Adjust the visibility of the toolbar required to move the fragment in Activity. In this project, Jetpack Navigation was used, so addOnDestinationChangedListener from navController was used.

        private fun initNavigationEvents() {
            val appBarConfiguration = AppBarConfiguration(
                setOf(
                    R.id.mainFragment,
                    R.id.fragmentA,
                    R.id.fragmentB,
                )
            )
    
            val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
            navController = navHostFragment.navController
    
            navController.addOnDestinationChangedListener { _, destination, _ ->
                when (destination.id) {
                    R.id.mainFragment, R.id.fragmentA, R.id.fragmentB -> {
                        mViewDataBinding.defaultToolbar.visibility = View.VISIBLE
                        mViewDataBinding.backstackToolbar.visibility = View.GONE
                    }
    
                    else -> {
                        mViewDataBinding.defaultToolbar.visibility = View.GONE
                        mViewDataBinding.backstackToolbar.visibility = View.VISIBLE
                    }
                }
            }
            ...
        }
    

    1. This is the part that caused the activity to be divided into two, and as I wrote at the top of the answer, it can be solved very simply if it is not used as an actionBar.

    Add DrawerLayout in Activity.

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context=".ui.MainActivity">
    
        <androidx.drawerlayout.widget.DrawerLayout
            android:id="@+id/drawer_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:openDrawer="start">
    
            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    
                // toolbars
    
                // FragmentContainerView
    
            </androidx.constraintlayout.widget.ConstraintLayout>
    
            <com.google.android.material.navigation.NavigationView
                android:id="@+id/navigation_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_gravity="start"
                android:layout_marginEnd="-64dp"
                android:fitsSystemWindows="true"
                app:headerLayout="@layout/navigation_left_drawer_header"
                app:menu="@menu/bottom_nav_menu" />
    
        </androidx.drawerlayout.widget.DrawerLayout>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    If toolbar is set to ActionBar, you can inflate the menu through the MenuProvider and specify the action of home through android.R.id.home in onMenuItemSelected.

    However, this method should only be used if only one toolbar is used, and if you want to use the toolbar differently for each fragment, it should not be designated as an ActionBar. So, the menu must be inflated on the toolbar without specifying the ActionBar.

    In Toolbar, the image of the Home button can be changed to toolbar.setNavigationIcon(), and the click event can be set with the toolbar.setNavigationOnClickListener.

    private fun initDefaultToolbar() {
        mViewDataBinding.defaultToolbar.inflateMenu(R.menu.toolbar_activity)
        mViewDataBinding.defaultToolbar.setNavigationIcon(R.drawable.ic_action_open_drawer)
    
        mViewDataBinding.defaultToolbar.setOnMenuItemClickListener {
            when (it.itemId) {
                R.id.item1 -> {
                    Toast.makeText(this@MainActivity, "item1 clicked", Toast.LENGTH_SHORT).show()
                    return@setOnMenuItemClickListener true
                }
    
                R.id.item2 -> {
                    Toast.makeText(this@MainActivity, "item2 clicked", Toast.LENGTH_SHORT).show()
                    return@setOnMenuItemClickListener true
                }
    
                else -> {
                    return@setOnMenuItemClickListener false
                }
            }
        }
    
        mViewDataBinding.defaultToolbar.setNavigationOnClickListener {
            mViewDataBinding.drawerLayout.openDrawer(GravityCompat.START)
        }
    }
    
    private fun initBackstackToolbar() {
        mViewDataBinding.backstackToolbar.setNavigationIcon(R.drawable.ic_action_back)
        mViewDataBinding.backstackToolbar.setNavigationOnClickListener {
            navController.popBackStack()
        }
    }
    

    1. You can easily customize it by adding ImageView to indicate logo and TextView to indicate the title within the toolbar and adjusting visibility as needed.

       <androidx.appcompat.widget.Toolbar
           android:id="@+id/default_toolbar"
           android:layout_width="match_parent"
           android:layout_height="?attr/actionBarSize"
           android:background="?attr/colorPrimary"
           app:layout_constraintTop_toTopOf="parent">
      
           <ImageView
               android:id="@+id/app_logo"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_gravity="center"
               android:src="@drawable/ic_launcher_foreground"
               android:visibility="@{!vm.titleVisibility}" />
      
           <TextView
               android:id="@+id/toolbar_title"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_gravity="center"
               android:text="@{vm.title}"
               android:visibility="@{vm.titleVisibility}" />
       </androidx.appcompat.widget.Toolbar>
      
       <androidx.appcompat.widget.Toolbar
           android:id="@+id/backstack_toolbar"
           android:layout_width="match_parent"
           android:layout_height="?attr/actionBarSize"
           android:background="?attr/colorPrimary"
           app:layout_constraintTop_toTopOf="parent"
           android:visibility="gone">
      
           <TextView
               android:id="@+id/toolbar_title2"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_gravity="center"
               android:text="@{vm.title}"
               android:visibility="@{vm.titleVisibility}" />
       </androidx.appcompat.widget.Toolbar>
      

    I implemented it by passing the title of the fragment from the fragment's onViewCreated to Activity and passing the data to the ViewModel and branching it.