I am finally adopting Navigation Architecture Components in a new project and I am already encountering some issues that the docs don't seem to address.
With an ActionBar menu setup in the activity, when I navigate to another fragment and then try to use the ActionBar menu I receive
java.lang.IllegalArgumentException: Navigation action/destination X cannot be found from the current destination
It would appear that I must also add actions from all possible destinations to goto all other destinations which seems like overkill, this just cannot be. There must be a solution to this problem that I am not finding.
I intend to open the app which inflates MainActivity and then MainFragment within the fragment element. The MainActivity should still handle top level navigation within the ActionBar. There is absolutely no reason I need to dupe menu click work in every fragment and define destinations to all other areas of the app.
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
private lateinit var appBarConfiguration: AppBarConfiguration
private var menu: Menu? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val controller by lazy { findNavController(R.id.fragment_container) }
navController = controller
val appBarConfig by lazy { AppBarConfiguration(navController.graph) }
appBarConfiguration = appBarConfig
setSupportActionBar(toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.appbar_menu, menu)
this.menu = menu
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
var direction: NavDirections? = null
when(item.itemId) {
R.id.action_messages -> {
direction = MainFragmentDirections.actionMainFragmentToMessagesFragment()
}
R.id.action_menu -> {
direction = MainFragmentDirections.actionMainFragmentToMessagesFragment()
}
else -> super.onOptionsItemSelected(item)
}
if (direction != null) navController.navigate(direction)
return true
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}
activity_main
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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/card_id_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:background="#color/colorPrimary"
android:elevation="4dp"
android:clipToPadding="false"
app:menu="#menu/appbar_menu"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:titleTextColor="#color/white" />
<fragment
android:id="#+id/fragment_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"
app:layout_constraintTop_toBottomOf="#+id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
MainFragment
class MainFragment : Fragment() {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
private var mActivity : MainActivity? = null
private var mView: View? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
_binding = FragmentMainBinding.inflate(inflater, container, false)
mView = binding.root
mActivity = (activity as MainActivity)
return mView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_dashboard.setOnClickListener {
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment())
}
}
}
fragment_main
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/content_test"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="Main Fragment"
android:textSize="24dp" />
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_dashboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="Dashboard" />
</androidx.constraintlayout.widget.ConstraintLayout>
DashboardFragment
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private var mActivity : MainActivity? = null
private var mView: View? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreateView(inflater, container, savedInstanceState)
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
mView = binding.root
mActivity = (activity as MainActivity)
return mView
}
}
fragment_dashboard
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/content_test"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="Dashboard Fragment"
android:textSize="24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
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"
android:id="#+id/nav_graph"
app:startDestination="#id/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.appbarnavigation.MainFragment"
android:label="Main" >
<action
android:id="#+id/action_mainFragment_to_messagesFragment"
app:destination="#id/action_messages" />
<action
android:id="#+id/action_mainFragment_to_menuFragment"
app:destination="#id/action_menu" />
<action
android:id="#+id/action_mainFragment_to_dashboardFragment"
app:destination="#id/dashboardFragment" />
</fragment>
<fragment
android:id="#+id/action_messages"
android:name="com.example.appbarnavigation.MessagesFragment"
android:label="Messages" />
<fragment
android:id="#+id/action_menu"
android:name="com.example.appbarnavigation.MenuFragment"
android:label="Menu" />
<fragment
android:id="#+id/dashboardFragment"
android:name="com.example.appbarnavigation.DashboardFragment"
android:label="Dashboard" />
</navigation>
appbar_menu
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/action_messages"
android:orderInCategory="1"
android:title="Messages"
android:icon="#drawable/ic_message"
app:showAsAction="always"/>
<item
android:id="#+id/action_menu"
android:orderInCategory="2"
android:title="Menu"
android:icon="#drawable/ic_menu"
app:showAsAction="always"/>
</menu>
This was resolved by defining and using global actions.
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/nav_graph"
app:startDestination="#id/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.appbarnavigation.MainFragment"
android:label="Main" >
<action
android:id="#+id/action_mainFragment_to_dashboardFragment"
app:destination="#id/dashboardFragment" />
</fragment>
<fragment
android:id="#+id/action_messages"
android:name="com.example.appbarnavigation.MessagesFragment"
android:label="Messages" />
<fragment
android:id="#+id/action_menu"
android:name="com.example.appbarnavigation.MenuFragment"
android:label="Menu" />
<fragment
android:id="#+id/dashboardFragment"
android:name="com.example.appbarnavigation.DashboardFragment"
android:label="Dashboard" />
<action android:id="#+id/action_global_action_messages" app:destination="#id/action_messages" />
<action android:id="#+id/action_global_action_menu" app:destination="#id/action_menu" />
</navigation>
and in the MainActivity
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
R.id.action_messages -> {
navController.navigate(R.id.action_global_action_messages)
true
}
R.id.action_menu -> {
navController.navigate(R.id.action_global_action_menu)
true
}
// This is used for menu buttons or anything not explicitly defined here
else -> {
super.onOptionsItemSelected(item)
}
}
Related
I have implemented BottomNavigationView with NavController but fragments keep reloading on each click, even with the new navigation library update. I also tried setOnNavigationItemReselectedListener but it doesn't trigger anything. What am I doing wrong ?
I know this question have been asked more than once but I feel like I tried them all and none of them work. I was hoping I could avoid reverting my code with old libraries or using boilerplate solutions.
Here is my code :
build.gradle (:app)
implementation 'androidx.navigation:navigation-runtime-ktx:2.5.2'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
HomeActivity.kt
class HomeActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener,
Serializable {
private lateinit var binding: ActivityHomeBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding= ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.home_container) as NavHostFragment
navController = navHostFragment.navController
binding.bottomNavigation.setupWithNavController(navController)
}
}
activity_home.xml
<RelativeLayout 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:fitsSystemWindows="true"
tools:openDrawer="start"
tools:context=".HomeActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/home_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="#+id/bottom_navigation"
app:layout_constraintTop_toBottomOf="#+id/appbar_layout"
app:navGraph="#navigation/nav_main" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="#menu/bottom_navigation_menu"
app:layout_constraintTop_toBottomOf="#+id/home_container"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.5"/>
</RelativeLayout>
nav_main.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/nav_main"
app:startDestination="#id/firstFragment">
<fragment
android:id="#+id/firstFragment"
android:name="com.projectName.ui.main.ContactsFragment"
tools:layout="#layout/fragment_contacts" />
<fragment
android:id="#+id/secondFragment"
android:name="com.projectName.SearchFragment"
android:label="#string/search"
tools:layout="#layout/search_fragment" />
<fragment
android:id="#+id/thirdFragment"
android:name="com.projectName.ui.main.NotificationsFragment"
tools:layout="#layout/notifications_fragment" />
<fragment
android:id="#+id/fourthFragment"
android:name="com.projectName.chat.ChatFragment"
tools:layout="#layout/fragment_main_page" />
</navigation>
bottom_navigation_menu.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/firstFragment"
android:enabled="true"
android:icon="#drawable/ic_baseline_contacts_24"
android:title=""
app:labelVisibilityMode="unlabeled"/>
<item
android:id="#+id/secondFragment"
android:enabled="true"
android:icon="#drawable/ic_search_white_24dp"
android:title=""
app:labelVisibilityMode="unlabeled"/>
<item
android:id="#+id/thirdFragment"
android:enabled="true"
android:icon="#drawable/ic_baseline_notifications_none_24"
android:title=""
app:labelVisibilityMode="unlabeled"/>
<item
android:id="#+id/fourthFragment"
android:enabled="true"
android:icon="#drawable/ic_message_white_24dp"
android:title=""
app:labelVisibilityMode="unlabeled"/>
</menu>
Fragments
class NotificationsFragment : Fragment() {
companion object {
fun newInstance() = NotificationsFragment()
}
private lateinit var viewModel: NotificationsViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewModel = ViewModelProvider(this).get(NotificationsViewModel::class.java)
return inflater.inflate(R.layout.notifications_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
I also gave every layout an ID like the document of savedInstateState suggests.
Thank you.
EDIT
I could not find a solution for my problem so I used BottomNavigationView with ViewPager2 :
HomePagerAdapter.kt
private val TAB_TITLES = arrayOf(
Constants.CONTACTS,
Constants.SEARCH,
Constants.NOTIFICATIONS,
Constants.CHATS
)
class HomePagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount(): Int = TAB_TITLES.size
override fun createFragment(position: Int): Fragment {
return if (getPageTitle(position) == Constants.CONTACTS) {
ContactsFragment.newInstance()
} else if (getPageTitle(position) == Constants.SEARCH) {
SearchFragment.newInstance()
} else if (getPageTitle(position) == Constants.NOTIFICATIONS) {
NotificationsFragment.newInstance()
} else {
ChatsFragment.newInstance()
}
}
private fun getPageTitle(position: Int): String {
return TAB_TITLES[position]
}
}
HomeActivity.kt
class HomeActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener,
Serializable, SearchFragment.OnPersonSelectedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
val homePagerAdapter = HomePagerAdapter(this)
val viewPager: ViewPager2 = findViewById(R.id.home_view_pager)
viewPager.adapter = homePagerAdapter
viewPager.isUserInputEnabled = false
viewPager.offscreenPageLimit = 1
viewPager.overScrollMode = ViewPager2.OVER_SCROLL_NEVER
findViewById<BottomNavigationView>(R.id.bottom_navigation)
.setOnItemSelectedListener {
when (it.itemId) {
R.id.firstFragment -> {
viewPager.setCurrentItem(0, false)
return#setOnItemSelectedListener true
}
R.id.secondFragment -> {
viewPager.setCurrentItem(1, false)
return#setOnItemSelectedListener true
}
R.id.thirdFragment -> {
viewPager.setCurrentItem(2, false)
return#setOnItemSelectedListener true
}
R.id.fourthFragment -> {
viewPager.setCurrentItem(3, false)
return#setOnItemSelectedListener true
}
else -> return#setOnItemSelectedListener false
}
}
}
}
activity_home.xml
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/home_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/bottom_navigation"
app:layout_constraintTop_toBottomOf="#+id/appbar_layout"
app:layout_behavior="#string/appbar_scrolling_view_behavior" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="#menu/bottom_navigation_menu"
app:layout_constraintTop_toBottomOf="#+id/home_view_pager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.5"/>
If I understood your problem right, you want that whenever you change from one fragment to another by using the bottom navigation bar that the fragments will perserve their state. I think this might be helpful https://developer.android.com/guide/navigation/multi-back-stacks. Every tab on the bottom navigation will have its own graph and while switching from one tab to another the state will be saved. Let me know if this helps you out!
I am new to using the navigation component for Android. Most of the examples I have seen online discuss fragment navigation within an activity but, in my case, I want to navigate within a base fragment. In the picture below, I would like to navigate from a login fragment to a signup fragment within the same settings tab but am running into issues and have tried various possible solutions. Any help would be appreciated as I am aware that I can certainly missing something.
java.lang.IllegalArgumentException: Navigation action/destination com.example.animaljournalapp:id/action_userAccountFragment_to_userSignUpFragment cannot be found from the current destination Destination(com.example.animaljournalapp:id/navigation_settings) label=Settings class=com.example.animaljournalapp.ui.settings.UserAccountFragment
I attempt to perform the navigation in the login fragment file through a listener I have attached to a "Sign Up" button:
UserAccountFragment.kt
class UserAccountFragment : Fragment() {
private lateinit var userAccountModel: UserAccountModel
private lateinit var navHostFrag: Fragment
private var _binding: FragmentUserAccountBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
userAccountModel =
ViewModelProvider(this).get(UserAccountModel::class.java)
navHostFrag = SettingsMainFragment()
_binding = FragmentUserAccountBinding.inflate(inflater, container, false)
val root: View = binding.root
val loginButton: Button = root.findViewById(R.id.login_button)
val signupButton: Button = root.findViewById(R.id.signup_button)
loginButton.setOnClickListener{ view: View ->
Toast.makeText(this.context, "Hi", Toast.LENGTH_LONG).show()
}
signupButton.setOnClickListener { view: View ->
val action = UserAccountFragmentDirections.actionUserAccountFragmentToUserSignUpFragment()
Navigation.findNavController(view).navigate(action)
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
The main activity has a navigation graph of the following:
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/navigation_home">
<fragment
android:id="#+id/navigation_home"
android:name="com.example.animaljournalapp.ui.home.HomeFragment"
android:label="#string/title_home"
tools:layout="#layout/fragment_home" />
<fragment
android:id="#+id/navigation_dashboard"
android:name="com.example.animaljournalapp.ui.dashboard.DashboardFragment"
android:label="#string/title_dashboard"
tools:layout="#layout/fragment_dashboard" />
<fragment
android:id="#+id/navigation_settings"
android:name="com.example.animaljournalapp.ui.settings.UserAccountFragment"
android:label="#string/title_settings"
tools:layout="#layout/fragment_settings_main" />
</navigation>
settings_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/settings_navigation"
app:startDestination="#id/userAccountFragment">
<fragment
android:id="#+id/userAccountFragment"
android:name="com.example.animaljournalapp.ui.settings.UserAccountFragment"
android:label="fragment_user_account"
tools:layout="#layout/fragment_user_account" >
<action
android:id="#+id/action_userAccountFragment_to_userSignUpFragment"
app:destination="#id/userSignUpFragment" />
</fragment>
<fragment
android:id="#+id/userSignUpFragment"
android:name="com.example.animaljournalapp.ui.settings.UserSignUpFragment"
android:label="UserSignUpFragment" />
</navigation>
fragment_settings_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
tools:context=".ui.settings.SettingsMainFragment">
<fragment
android:id="#+id/settings_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="#navigation/settings_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
I'm trying to combine NavigationDrawer with a Toolbar that has a refresh Menu option:
The problem I'm encountering is that I cannot make the Toolbar show the menu button.
My MainActivity only holds a Fragment. activity_main.xml:
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/main_nav_container"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/main" />
MainActivity.kt:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
nav_main:
<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_main"
app:startDestination="#id/drawerFragment"
>
<fragment
android:id="#+id/drawerFragment"
android:name="com.example.rocketman.drawer.DrawerFragment"
android:label="DrawerFragment"
tools:layout="#layout/fragment_drawer"
/>
</navigation>
Basically my MainActivity navigates directly into a DrawerFragment, which is here:
<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"
android:orientation="vertical"
tools:context="com.example.rocketman.drawer.DrawerFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.rocketman.drawer.DrawerFragment">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="#+id/toolbar_home"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/midnight_blue"
app:titleTextColor="#color/white"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/drawer_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<com.google.android.material.navigation.NavigationView
android:id="#+id/drawer_nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="#layout/drawer_header"
app:menu="#menu/drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
And finally, my DrawerFragment:
private const val KEY_SELECTED_DRAWER_ITEM = "DRAWER_SELECTED_ITEM_ID_KEY"
class DrawerFragment: Fragment() {
private lateinit var binding: FragmentDrawerBinding
private var drawerSelectedItemId = R.id.nav_home
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentDrawerBinding.inflate(inflater)
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar_home)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
savedInstanceState?.let {
drawerSelectedItemId = it.getInt(KEY_SELECTED_DRAWER_ITEM, drawerSelectedItemId)
}
setupDrawer()
setBackPressedHandler()
}
private fun setBackPressedHandler() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
} else {
findNavController().popBackStack()
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_SELECTED_DRAWER_ITEM, drawerSelectedItemId)
super.onSaveInstanceState(outState)
}
private fun setupDrawer() {
val controller = binding.drawerNavView.setupWithNavController(
childFragmentManager,
findNavController(),
listOf(
//all the items on the Drawer have their own navigation graph
R.navigation.home,
R.navigation.rocket,
R.navigation.company
),
R.id.drawer_container,
drawerSelectedItemId,
requireActivity().intent
)
controller.observe(
viewLifecycleOwner,
{ navController ->
NavigationUI.setupWithNavController(
binding.toolbarHome,
navController,
binding.drawerLayout
)
drawerSelectedItemId = navController.graph.id
}
)
}
}
In other words, the Drawer has three items: home, rocket and company as can be seen here, menu/drawer:
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#+id/nav_home"
android:checked="true"
android:checkable="true"
android:icon="#drawable/ic_home"
android:title="#string/nav_menu_home"
/>
<item
android:id="#+id/nav_rocket"
android:checkable="true"
android:icon="#drawable/ic_rocket"
android:title="#string/nav_menu_rocket_list"
/>
<item
android:id="#+id/nav_company"
android:checkable="true"
android:icon="#drawable/ic_company"
android:title="#string/nav_menu_company_data"
/>
</menu>
Each of home, rocket and company have their own navigation graph, for example navigation/company looks like this:
<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_company"
app:startDestination="#id/companyDataFragment">
<fragment
android:id="#+id/companyDataFragment"
android:name="com.example.rocketman.company.CompanyFragment"
android:label="#string/nav_menu_company_data"
tools:layout="#layout/fragment_company_data"
/>
</navigation>
And the CompanyFragment is straight forward:
class CompanyFragment: Fragment() {
private lateinit var binding: FragmentCompanyDataBinding
private val vm by lazy {
ViewModelProvider(this).get(CompanyVM::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
Repo.init(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCompanyDataBinding.inflate(inflater)
return binding.root
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.company, menu)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupObservers()
}
private fun setupObservers() {
//observing
}
}
That part works.
However, I want to create a menu action for Company. So first I create a menu item menu/company:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/update"
android:icon="#drawable/ic_baseline_refresh_24"
android:title="#string/menu_company_update"
app:showAsAction="ifRoom|withText"
/>
</menu>
Then in CompanyFragment I call override onCreate() { setHasOptionsMenu(true) } and override onCreateOptionsMenu() { inflater.inflate(R.menu.company, menu }.
This should create the options menu for the Toolbar. However, the Fragment only shows the drawer menu icon but not the options menu icon.
I can add a Toolbar to the content Fragment itself but then the app has two Toolbars, the one with the drawer and the one with option items:
How do I make the Toolbar have both the NavigationDrawer and option items?
Still many people continue to use ActionBar, we don't have to use it any more. And if I were you, I would not use ActionBar and setHasOptionsMenu(true) which is related to ActionBar, but just would use a Toolbar.
Just delete setHasOptionsMenu(true), onCreateOptionsMenu() and onOptionsItemSelected() (if you have it).
Instead, use Toolbar.inflateMenu() and Toolbar.setOnMenuItemClickListener() directly on your Toolbar.
From what I understand from your question, you want to display overflow menu along with drawer menu.
The showAsAction option in the menu xml file is what you can use to control how that single menu option will show up in the actionbar.
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/update"
android:icon="#drawable/ic_baseline_refresh_24"
android:title="#string/menu_company_update"
app:showAsAction="ifRoom|withText"
/>
</menu>
change this to
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="#+id/update"
android:icon="#drawable/ic_baseline_refresh_24"
android:title="#string/menu_company_update"
app:showAsAction="never"
/>
</menu>
This will show the menu item in the Overflow menu.
Iv been stuck in trying to add the Navigation Component to my app. Specifically to my toolbar menu items.
I have been following the navigation migrate documentation on Android's site but I am getting a but confused on how it works or even to set it up.
This is a single-activity multi-fragment architecture.
The app would start in the MainFragment and then when tapping the menu items in the toolbar, such as Main Frag to Search Movie Frag. The user would be able to navigate using the menu items from anywhere in the app (e.g. if they click the home icon, they should be sent to the home screen form anywhere in the app. Still thinking if thats how it should be done).
The main thing is I don't know how to properly attach the Navigation Component to the menu items.
App
Nav Graph
nav_graph.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/nav_graph"
app:startDestination="#id/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.movieapp.ui.main.MainFragment"
android:label="MainFragment">
<action
android:id="#+id/action_mainFragment_to_searchMovieFragment"
app:destination="#id/searchMovieFragment" />
</fragment>
<fragment
android:id="#+id/searchMovieFragment"
android:name="com.example.movieapp.ui.search.SearchMovieFragment"
android:label="SearchMovieFragment" />
</navigation>
main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<include
layout="#layout/appbar"
android:id="#+id/mainToolBar"/>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#id/mainToolBar"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var toolbar: Toolbar
private val navController by lazy {
(supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
}
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
toolbar = findViewById(R.id.mainToolBar)
setSupportActionBar(toolbar)
appBarConfiguration = AppBarConfiguration(navController.graph, null)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val inflater: MenuInflater = menuInflater
inflater.inflate(R.menu.main_menu_bar, menu)
return true
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
}
MainFragment.kt
class MainFragment : Fragment() {
companion object {
fun newInstance() = MainFragment()
}
private lateinit var viewModel: MainViewModel
private lateinit var binding: MainFragmentBinding
private lateinit var navController: NavController
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
setHasOptionsMenu(true)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
// navController.navigate(R.id.action_mainFragment_to_searchMovieFragment)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewModel = viewModel
// TODO: Use the ViewModel
}
}
main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.movieapp.ui.main.MainViewModel" />
</data>
<LinearLayout
android:orientation="vertical"
android:id="#+id/main_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.main.MainFragment">
<TextView
android:id="#+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MainFragment1" />
</LinearLayout>
</layout>
The id for a menu item in your file main_menu_bar.xml needs to match the id for the destination(fragment) specified in your navigation graph (nav_graph.xml).
Your main_menu_bar.xml should look something like this:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group android:checkableBehavior="single">
<item
android:id="#id/searchMovieFragment"
android:icon="#drawable/search"
android:title="#string/menu_search" />
<item
android:id="#id/mainFragment"
android:icon="#drawable/ic_menu_camera"
android:title="#string/menu_home" />
</group>
</menu>
Also, you need to override the onOptionsItemSelected(MenuItem) method and associate your menu items with destinations. Like so:
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
}
Reference
You can add a Toolbar on top of the most basic FragmentContainerView, access this toolbar from other fragments, and change the toolbar I created separately according to the fragment in it while opening the relevant fragment as follows.
toolbar = requireActivity().findViewById(R.id.toolbar_base)
toolbar?.menu?.clear()
toolbar?.title = ""
val toolbarFlowFragment = context?.let { getInflateLayout(it, R.layout.toolbar_profile) }
toolbar?.addView(toolbarFlowFragment)
I am using jetpack navigation in two different activities each with its navigation host. I have Main activity which everything just works fine and Setup activity which have 2 fragments, one has a button which should navigate to other fragment when clicked .
The activity's code is as follows:
class SetupActivity : AppCompatActivity() {
private val navController by lazy { findNavController(R.id.nav_host_fragment) }
private lateinit var appBarConfig: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<ActivitySetupBinding>(this, R.layout.activity_setup)
setSupportActionBar(toolbar)
appBarConfig = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfig)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfig) || super.onSupportNavigateUp()
}
}
Below is activity's xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context=".ui.SetupActivity"
tools:showIn="#layout/activity_setup">
<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_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/setup" />
</androidx.constraintlayout.widget.ConstraintLayout>
Below is code of first fragment which is also a start Destination
class ConfirmClassFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentConfirmClassBinding.inflate(inflater, container, false)
binding.fragment = this
return binding.root
}
fun navigate() {
toast("clicked") //this get called
val direction = ConfirmClassFragmentDirections.actionConfirmToSetup()
findNavController().navigate(direction) //THE PROBLEM IS HERE
//findNavController().navigate(R.id.setup)
}
}
I am using data binding to call fun navigate() as below:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="fragment"
type="com.nux.ui.fragments.ConfirmClassFragment" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="#dimen/spacing_middle">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="#{() -> fragment.navigate()}"
android:text="#string/continue_" />
</LinearLayout>
</layout>
and this how navigation xml looks like:
<?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/setup"
app:startDestination="#id/confirm">
<fragment
android:id="#+id/setup"
android:name="com.nux.ui.fragments.SetInfoFragment"
android:label="#string/setup"
tools:layout="#layout/fragment_set_info" />
<fragment
android:id="#+id/confirm"
android:name="com.nux.ui.fragments.ConfirmClassFragment"
android:label="#string/data_confirmation"
tools:layout="#layout/fragment_confirm_class">
<action
android:id="#+id/action_confirm_to_setup"
app:destination="#id/setup" />
</fragment>
</navigation>
When I click button fun gets called but nothing happens! What could be wrong?
From the code i can see you have the same id for 3 elements in the navigation graph.
#id/setup should only be used once and please change the name of the navigation graph from setup to something else.