DevinM
DevinM

Reputation: 1322

Navigation Component - ActionBar handle in Activity only

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>

Upvotes: 1

Views: 978

Answers (1)

DevinM
DevinM

Reputation: 1322

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

Upvotes: 1

Related Questions