Categories:
Search articles

Search for articles

Sorry, but we couldn't find any matches...

But perhaps we can interest you in one of our more popular articles?

Introduction to the core concepts of Android Navigation

Sep 17, 2020

How to build native Android apps on Codemagic

How to build native Android apps on Codemagic

Use codemagic.yaml file to keep your build configurations more organized and manage all your workflows
Read more

Written by Souvik Biswas

Navigation is one of the core concepts to master if you are working on any Android app that has a complex navigation system. Properly handled navigation also helps to reduce any security flaws in your app through the implementation of functions like authenticating the user before navigating to any sensitive content in the app or preventing any sensitive information from being shared with other apps on the device.

There have been constant improvements to the navigation system used in Android over the past year, and this has introduced a lot of new concepts.

In this article, I will cover some of the important concepts of Android Navigation that come with the Android Jetpack's Navigation component.

What is the Jetpack Navigation component?

The Android Jetpack Navigation component is a suite of libraries, tooling, and guidance that provides a robust navigation framework for in-app navigation.

The Navigation component also provides some other benefits, like:

  • Handling fragment transactions
  • Handling Up and Back actions correctly by default
  • Implementing deep linking
  • Navigation UI patterns, like navigation drawer, bottom navigation, etc.
  • Support for Safe Args
  • ViewModel support
  • Passing data using Android Bundle
  • Listening to navigation changes
  • Animating navigation
  • Testing navigation

The Navigation component works both with Activities and Fragments. In the sample app, we will mostly be working with Fragments, as they also provide good integration with Android's underlying lifecycle.

When you navigate to different Activities, the application maintains a stack known as the back stack. The back stack helps to handle the behavior of the Up and Back buttons inside the app. The Fragments also maintain a back stack, but in this case, the entire Fragment back stack is contained within the Activity.

The Navigation component adheres to an established set of Android Navigation principles. Let's take a look at them.

Principles of Android Navigation

To give users a consistent and intuitive user experience across a wide variety of Android apps, there are some core principles followed by navigation. The Navigation component is designed to implement these principles by default.

  • There's always a starting place: Every Android app should have a fixed start destination. This is the screen the user sees every time they open the app. This is also the last screen they see when they navigate back from other screens within the app by using the Up button.

  • A back stack is always maintained: When a user navigates to different Activities or Fragments inside the app, there is a back stack that is always maintained. The back stack helps in navigating the user back to the correct destination from where they came as they go Up or Back. It follows a Last-In-First-Out (LIFO) structure.

    • Start Destination is present lower in the stack
    • Current Destination is present at the top of the stack
    • New Destinations are pushed to the top of the stack
    • When going back, the current destination is popped off the stack
  • Up versus Back: Both the Up and Back buttons act in a similar way. The difference is that the Up button is only used for navigating inside an app—it will never navigate you out of the app. So, the Up button should never be shown on the first screen of the app. However, the Back button can take you outside of the app to the screen that is present the next level down in the navigation hierarchy.

  • Deep linking simulates manual navigation: Whether you use deep linking or manually navigate to a certain screen inside an app, the back stack is populated with the same screens. So, if you have navigated to a certain page using deep linking, when you go back, the same back stack will be used as when navigating to that screen manually.

Key parts of the Navigation component

The Navigation component consists of three main parts:

  • Navigation graph: This manages your app's navigation. It is a resource file that consists of the destinations along with the actions, which are used for navigating to another destination from the current one.

  • NavHost: An empty container that displays destinations from your navigation graph.

  • NavController: This helps in managing app navigation within a NavHost. It orchestrates the swapping of destination content in the NavHost as users move throughout your app.

The Navigation Editor helps in visualizing and editing the whole navigation system used in your app, which consists of a navigation graph, arguments, transition animations, and deep links.

Our goal

We will be building an app called Quotes Trivia, in which the user has to guess the correct person who said the displayed memorable quote.

As the main focus of this article is Navigation, we will mostly be covering concepts related to that. But if you want to take a closer look at the UI and logic of the app, you may head over to the GitHub repo of the sample app, which is linked at the end of this article.

Set up the Navigation component

We will be using the Android KTX version of the dependencies, as we are using the Kotlin language in our project. But if you are using Java, non-ktx versions of the same dependencies are also available.

To use the Navigation component, add the following dependencies to your build.gradle (app) file:

dependencies {
    def nav_version = "2.3.0"

    implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
    implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}

Add the Navigation graph

Create a new XML file by right-clicking on the res folder and selecting New > Android Resource File. Select the Resource type as Navigation, and use the file name navigation.

You have to link the navigation.xml file in your activity_main.xml file by adding a NavHostFragment to use it for navigating across different Fragments of your app.

<!-- activity_main.xml -->

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <!-- Add the following for linking the navigation resource file -->
        <androidx.fragment.app.FragmentContainerView
            android:id="@+id/myNavHostFragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/navigation" />

    </LinearLayout>

</layout>

Add a fragment to the graph

Go the Navigation Editor of the navigation.xml file. Click on the New Destination button, and select the screen that you want to add to the Navigation graph.

For navigation from the current destination to any other, you first need to create an action in the Navigation graph.

You have to write the Kotlin code explicitly for triggering the action that you have defined in the Navigation graph.

binding.startButton.setOnClickListener { view: View ->
    view.findNavController().navigate(R.id.action_welcomeFragment_to_triviaFragment)
}

Conditional Navigation

We will use conditional navigation for deciding whether to navigate the user to WonFragment or LostFragment. For our trivia game, the user who manages to answer around 80% of the questions correctly will be considered the winner.

Set up the actions from the TriviaFragment to the two Fragments to which navigation is possible.

Triggering the actions from the Kotlin code:

if (correctAnswers < round(0.8 * numQuestions)) {
    view.findNavController().navigate(R.id.action_triviaFragment_to_lostFragment)
} else {
    view.findNavController().navigate(R.id.action_triviaFragment_to_wonFragment)
}

Manipulate the back stack

If you try to go back from the WonFragment or LostFragment, the app will take you to the TriviaFragment. But this shouldn't be the default action, as the user might want to go back directly to the home screen. Also, there are NEXT MATCH and PLAY AGAIN buttons for if someone wants to play the game again.

So, to take the user directly from the WonFragment or LostFragment to the WelcomeFragment, we have to manipulate the back stack. This can be done easily by setting the Pop Behavior of any action from the Navigation Editor.

We will set the pop behavior of the action_triviaFragment_to_wonFragment and action_triviaFragment_to_lostFragment to popUpTo TriviaFragment (inclusive).

Using Safe Args

Safe Args is a Gradle plugin that generates simple object and builder classes for type-safe navigation and also simplifies argument passing.

Add the following classpath to your project's top-level build.gradle file:

dependencies {
    def nav_version = "2.3.0"
    classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}

Apply the plugin in your app-level build.gradle file:

apply plugin: "androidx.navigation.safeargs.kotlin"

This generates Kotlin code suitable for Kotlin-only modules. If you are using Java-language or mixed Java and Kotlin modules, then use the following:

apply plugin: "androidx.navigation.safeargs"

Do a clean build of the project.

Now, you can replace the the id(s) used for specifying navigation actions with the generated NavDirections classes, like this:

binding.startButton.setOnClickListener { view: View ->
    view.findNavController().navigate(WelcomeFragmentDirections.actionWelcomeFragmentToTriviaFragment())
}

Pass data between fragments

You can add the arguments that you want to pass directly from the Navigation Editor by selecting a Fragment.

Do a clean build.

For passing the arguments from a Fragment, you have to modify the Kotlin code as follows:

view.findNavController().navigate(TriviaFragmentDirections.actionTriviaFragmentToWonFragment(numQuestions, correctAnswers))

Android Bundle is used for retrieving the arguments in the navigated destination:

val args = WonFragmentArgs.fromBundle(requireArguments())

// Setting the values to the TextView(s)
correctAnswersText.text = args.numCorrect.toString()
totalQuestionsText.text = args.numQuestions.toString()

Add support for the Up button

The NavigationUI can handle the Up button behavior by itself so that you don't have to manage it explicitly. It also automatically hides the Up button when we are on the home screen of the app.

Inside the MainActivity, link the NavController to the ActionBar:

val navHostFragment = supportFragmentManager.findFragmentById(R.id.myNavHostFragment) as NavHostFragment
val navController = navHostFragment.navController
NavigationUI.setupActionBarWithNavController(this, navController)

Then, override the onSupportNavigateUp method from the activity:

override fun onSupportNavigateUp(): Boolean {
    val navController = this.findNavController(R.id.myNavHostFragment)
    return navController.navigateUp()
}

This will take care of all the navigation between different Fragments of this Activity.

Add an overflow menu

Let's add an overflow menu that will navigate the user to the AboutFragment. This would only be shown in the WelcomeFragment of our app.

Add the AboutFragment to the Navigation graph, but do not create any action for it.

Create a new menu resource called overflow_menu by right-clicking on res and selecting New > Android Resource File.

Declare an item with the same id as that of the Fragment you want it to navigate to.

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/aboutFragment"
        android:title="@string/about_title" />
</menu>

To show the menu only in the WelcomeFragment, add the following to the Fragment:

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // ...

    // Add this line
    setHasOptionsMenu(true)

    return binding.root
}

Override the onCreateOptionsMenu and inflate the menu resource:

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
    super.onCreateOptionsMenu(menu, inflater)
    inflater.inflate(R.menu.overflow_menu, menu)
}

Override the onOptionsItemSelected to connect it to our NavigationUI:

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return NavigationUI.onNavDestinationSelected(
        item,
        requireView().findNavController()
    ) || super.onOptionsItemSelected(item)
}

Add Navigation Drawer

The Navigation Drawer component is a part of the Android Material Design Library. It is a UI panel that can be opened by swiping from the left edge of the screen or by clicking on the menu icon present in the ActionBar. It is typically present in apps with five or more screens.

Add the Gradle dependency of Material Design to the app-level build.gradle file:

dependencies {
    def material_version = "1.2.1"

    implementation "com.google.android.material:material:$material_version"
}

Add one more Fragment to the Navigation graph called RulesFragment.

Create a new menu resource named navdrawer_menu, and add two item elements to it.

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/rulesFragment"
        android:icon="@drawable/ic_rules"
        android:title="@string/rules_title" />
    <item
        android:id="@+id/aboutFragment"
        android:icon="@drawable/ic_about"
        android:title="@string/about_title" />
</menu>

In activity_main.xml, wrap the LinearLayout with the DrawerLayout:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.drawerlayout.widget.DrawerLayout
        android:id="@+id/drawerLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <!-- ... -->

        </LinearLayout>
        
    </androidx.drawerlayout.widget.DrawerLayout>

</layout>

Add the NavigationView at the bottom of the DrawerLayout.

<com.google.android.material.navigation.NavigationView
    android:id="@+id/navView"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    app:menu="@menu/navdrawer_menu" />

Now, go to the MainActivity:

  • Add two variables:

    private lateinit var drawerLayout: DrawerLayout
    private lateinit var appBarConfiguration: AppBarConfiguration
    
  • Initialize the DrawerLayout inside onCreate:

    drawerLayout = binding.drawerLayout
    
  • Modify to include the drawer:

    NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
    
  • Create an AppBarConfiguration and hook the NavigationUI up to the navigation view:

    appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)
    NavigationUI.setupWithNavController(binding.navView, navController)
    
  • In the onSupportNavigateUp method, replace the following:

    return navController.navigateUp()
    

    with this:

    return NavigationUI.navigateUp(navController, drawerLayout)
    

    This helps in replacing the Up button with the menu button when we come to the first screen of the app.

You can add a headerLayout property to the NavigationView inside activity_main.xml:

<com.google.android.material.navigation.NavigationView
    android:id="@+id/navView"
    app:headerLayout="@layout/nav_header"
    app:menu="@menu/navdrawer_menu" />

The nav_header.xml file looks like this:

<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/navHeader"
    android:layout_width="match_parent"
    android:layout_height="192dp"
    android:background="?attr/colorPrimary"
    android:contentDescription="@string/cover_image_desc"
    android:padding="16dp"
    android:theme="@style/ThemeOverlay.AppCompat.Dark">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/trivia_cover" />
    
</androidx.constraintlayout.widget.ConstraintLayout>

Listen to navigation changes

Though you cannot see the menu button on pages other than the first screen, you will still be able to access the Navigation Drawer by swiping from the left edge of the screen on other pages.

In order to fix this issue and prevent it from opening on any other pages, we have to listen to the navigation changes and only enable the drawer on the first screen.

In your MainActivity file inside the onCreate method, add the following:

navController.addOnDestinationChangedListener { nc: NavController, nd: NavDestination, _: Bundle? ->
    if (nd.id == nc.graph.startDestination) {
        drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
    } else {
        drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
    }
}

Now, you will not be able to open the Navigation Drawer from any pages other than the first screen.

Add a share button

You will always like to share your winning score with your friends. So, let's add a Share button to our app using Intent.

Intents can be primarily of two types:

  • Explicit Intent: For launching Activities within the same app
  • Implicit Intent: This helps to provide an abstract description of the action that you want to perform, which is used for launching Activities of other apps on the device

If multiple Android apps that can handle a specific implicit intent are present on the user's device, then a chooser pops up.

Create a new menu resource file called share_menu.

Add a menu item to it:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/share"
        android:enabled="true"
        android:icon="@drawable/ic_share"
        android:title="@string/share_title"
        android:visible="true"
        app:showAsAction="ifRoom" />

</menu>

We want to show the share icon only in the WonFragment, so we will need to set up only in that Fragment.

Inside onCreateView add:

setHasOptionsMenu(true)

Create a method to define the Intent:

private fun getShareIntent(): Intent {
    val args = WonFragmentArgs.fromBundle(requireArguments())
    val shareIntent = Intent(Intent.ACTION_SEND)
    shareIntent.setType("text/plain")
        .putExtra(
            Intent.EXTRA_TEXT,
            getString(R.string.share_text, args.numCorrect, args.numQuestions)
        )
    return shareIntent
}

Method for starting the Activity:

private fun shareSuccess() {
    startActivity(getShareIntent())
}

Override the onCreateOptionsMenu method, inflate the menu resource, and show the share button only when there is an app installed on the user's device that can handle that implicit intent.

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
    super.onCreateOptionsMenu(menu, inflater)
    inflater.inflate(R.menu.share_menu, menu)

    if (null == getShareIntent().resolveActivity(requireActivity().packageManager)) {
        menu.findItem(R.id.share)?.isVisible = false
    }
}

Override the onOptionsItemSelected method to handle the action when the share button is clicked.

override fun onOptionsItemSelected(item: MenuItem): Boolean {
    when (item.itemId) {
        R.id.share -> shareSuccess()
    }

    return super.onOptionsItemSelected(item)
}

Animate navigation

You can apply different types of animations to the Navigation.

First of all, create a new animation resource file by right-clicking on the res folder and selecting New > Android Resource File.

Let's define a fading animation:

<!-- fade out animation -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="@android:integer/config_mediumAnimTime"
        android:fromAlpha="1.0"
        android:toAlpha="0.0" />
</set>

A sliding animation can be defined as follows:

<!-- slide in left animation -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="@android:integer/config_shortAnimTime"
        android:fromXDelta="-100%"
        android:fromYDelta="0%"
        android:toXDelta="0%"
        android:toYDelta="0%" />
</set>

You can use the Navigation Editor for applying the animations directly.

After applying all animations, the app looks like this:

Conclusion

The Android Navigation Component makes the whole navigation system of the app easily manageable. The Navigation Editor, along with the navigation graph, gives a good overview of the destinations, actions, arguments, and animations used in the app. Though we have covered most of the core features in this article, the Navigation component has a lot more awesome features to explore.

Learn More


Souvik Biswas is a passionate Mobile App Developer (Android and Flutter). He has worked on a number of mobile apps throughout his journey. Loves open source contribution on GitHub. He is currently pursuing a B.Tech degree in Computer Science and Engineering from Indian Institute of Information Technology Kalyani. He also writes Flutter articles on Medium - Flutter Community.

How did you like this article?

Oops, your feedback wasn't sent

Latest articles

Show more posts