The previous chapter described the Android Jetpack Navigation Component and how it integrates with the navigation graphing features of Android Studio to provide an easy way to implement navigation between the screens of an Android app. In this chapter, a new Android Studio project will be created that uses these navigation features to implement an example app containing multiple screens. In addition to demonstrating the use of the Android Studio navigation graph editor, the example project will also implement the passing of data between origin and destination screens using type-safe arguments.
Creating the NavigationDemo Project
Select the New Project option from the welcome screen and, within the resulting new project dialog, choose the Empty Views Activity template before clicking on the Next button.
Enter NavigationDemo into the Name field and specify com.ebookfrenzy.navigationdemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Java.
Adding Navigation to the Build Configuration
A new Empty Views Activity project does not include the Navigation component libraries in the build configuration files by default. Before performing any other tasks, therefore, the first step is to modify the app level build.gradle.kts file. Locate this file in the Project tool window (Gradle Scripts -> build.gradle.kts (Module :app)), double-click on it to load it into the code editor, and modify the dependencies section to add the navigation libraries. Also, take this opportunity to enable view binding for this module:
android {
buildFeatures {
viewBinding = true
}
.
.
dependencies {
implementation ("androidx.navigation:navigation-fragment:2.6.0")
implementation ("androidx.navigation:navigation-ui:2.6.0")
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
.
.
}
Code language: Gradle (gradle)
Note that newer versions of these libraries may have been released since this book was published. After adding the navigation dependencies to the file, click on the Sync Now link to resynchronize the build configuration for the project.
Creating the Navigation Graph Resource File
With the navigation libraries added to the build configuration, the navigation graph resource file can now be added to the project. As outlined in “An Overview of the Navigation Architecture Component”, this is an XML file containing the fragments and activities through which the user will be able to navigate, together with the actions to perform the transitions and any data to be passed between destinations.
Within the Project tool window, locate the res folder (app -> res), right-click on it, and select the New ->Android Resource File menu option:
After selecting the menu item, the New Resource File dialog will appear. In this dialog, name the file navigation_ graph and change the Resource type menu to Navigation as outlined in Figure 41-2 before clicking on the OK button to create the file.
After the navigation graph resource file has been added to the project, it will appear in the main panel, ready for adding new destinations. Switch the editor to Code mode and review the XML for the graph before any destinations are added:
<?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/navigation_graph">
</navigation>
Code language: HTML, XML (xml)
Switch back to Design mode within the editor and note that the Host section of the Destinations panel indicates that no navigation host fragments have been detected within the project:
Before adding any destinations to the navigation graph, the next step is to add a navigation host fragment to the project.
Declaring a Navigation Host
For this project, the navigation host fragment will be contained within the user interface layout of the main activity. First, locate the main activity layout file in the Project tool window (app -> res -> layout -> activity_ main.xml), load it into the layout editor tool, and delete the default TextView component.
With the layout editor in Design mode, drag a NavHostFragment element from the Containers section of the Palette and drop it onto the container area of the activity layout, as indicated by the arrow in Figure 41-4:
Select the navigation_graph.xml file created in the previous section from the resulting Navigation Graphs dialog and click on the OK button.
With the newly added NavHostFragment instance selected in the layout, use the Attributes tool window to change the element’s ID to demo_nav_host_fragment before clicking on the Infer constraints button.
Switch the layout editor to Code mode and review the XML file. Note that the editor has correctly configured the navigation graph property to reference the navigation_graph.xml file and that the defaultNavHost property has been set to true:
<?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=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/demo_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="409dp"
android:layout_height="729dp"
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/navigation_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
Code language: HTML, XML (xml)
Return to the navigation_graph.xml file and confirm that the NavHostFragment instance has been detected (it may be necessary to close and reopen the file before the change appears):
Adding Navigation Destinations
Remaining in the navigation graph, it is time to add the first destination. Click on the new destination button as shown in Figure 41-6 to select or create a destination:
Next, select the Create new destination option from the menu. In the resulting dialog, select the Fragment (Blank) template, name the new fragment FirstFragment and the layout fragment_first before clicking on the Finish button. After a short delay while the project rebuilds, the new fragment will appear as a destination within the graph, as shown in Figure 41-7:
The home icon above the destination node indicates this is the start destination. This means the destination will be the first displayed when the NavHostFragment activity is created. To change the start destination to another, select that node in the graph and click on the home button in the toolbar.
Review the XML content of the navigation graph by switching the editor to Code mode:
<?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/navigation_graph"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.ebookfrenzy.navigationdemo.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first" />
</navigation>
Code language: HTML, XML (xml)
Before any navigation can be performed, the graph needs at least one more destination. Repeat the above steps to add a fragment named SecondFragment with the layout file named fragment_second. The new fragment will appear as another destination within the graph, as shown in Figure 41-8:
Designing the Destination Fragment Layouts
Before adding actions to navigate between destinations, now is a good time to add some user interface components to the two destination fragments in the graph. Begin by double-clicking on the firstFragment destination so that the fragment_first.xml file loads into the layout editor, then select and delete the default TextView instance. Within the Component Tree panel, right-click on the FrameLayout entry and select the Convert from FrameLayout to ConstraintLayout menu option, accepting the default settings in the resulting conversion dialog:
Using the Attributes tool window, change the ID of the ConstraintLayout to constraintLayout, then drag and drop Button and Plain Text EditText widgets onto the layout so that it resembles that shown in Figure 41-10 below:
Once the views are correctly positioned, click on the Infer constraints button in the toolbar to add any missing constraints to the layout. Select the EditText view and use the Attributes tool window to delete the default “Name” text and change the widget’s ID to userText. Next, change the button text property to read “Navigate” and extract it to a string resource.
Return to the navigation_graph.xml file and double-click on the secondFragment destination to load the fragment_second.xml file into the layout editor. Select and delete the default TextView instance and repeat the above steps to convert the FrameLayout to a ConstraintLayout, changing the id to constraintLayout2. Next, drag and drop a new TextView widget to position it in the center of the layout and click on the Infer constraints button to add any missing constraints. With the new TextView selected, use the Attributes panel to change the ID to argText.
Adding an Action to the Navigation Graph
Now that the two destinations have been added to the graph and the corresponding user interface layouts are designed, the project needs a way for the user to navigate from the first fragment to the second. This will be achieved by adding an action to the graph, which can then be referenced from within the app code.
To establish an action connection with the first fragment as the origin and the second fragment as the destination, open the navigation graph and hover the mouse pointer over the vertical center of the right-hand edge of the firstFragment destination so that a circle appears as highlighted in Figure 41-11:
Click within the circle and drag the resulting line to the secondFragment destination:
Release the line to establish the action connection between the origin and destination, at which point the line will change into an arrow, as shown in Figure 41-13:
An action connection may be deleted anytime by selecting it and pressing the keyboard Delete key. With the arrow selected, review the properties available within the Attributes tool window and change the ID to mainToSecond. This is the ID by which the action will be referenced within the code. Switch the editor to Code mode and note that the action is now included within the 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/navigation_graph"
app:startDestination="@id/firstFragment">
<fragment
android:id="@+id/firstFragment"
android:name="com.ebookfrenzy.navigationdemo.FirstFragment"
android:label="fragment_first"
tools:layout="@layout/fragment_first" >
<action
android:id="@+id/mainToSecond"
app:destination="@id/secondFragment" />
</fragment>
<fragment
android:id="@+id/secondFragment"
android:name="com.ebookfrenzy.navigationdemo.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" />
</navigation>
Code language: HTML, XML (xml)
Implement the OnFragmentInteractionListener
Before adding code to trigger the action, the MainActivity class must be modified to implement the OnFragmentInteractionListener interface. This interface was generated within the Fragment classes when the blank fragments were created within the navigation graph editor. To conform to the interface, the activity needs a method named onFragmentInteraction() to implement communication between the fragment and the activity. Edit the MainActivity.java file and modify it so that it reads as follows:
.
.
import android.net.Uri;
.
.
public class MainActivity extends AppCompatActivity implements SecondFragment.OnFragmentInteractionListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onFragmentInteraction(Uri uri) {
}
}
Code language: Java (java)
If Android Studio reports that OnFragmentInteractionListener is undefined (some versions of Android Studio add it automatically, while others do not), edit the SecondFragment.java file and add the following:
.
.
import android.net.Uri;
.
.
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
void onFragmentInteraction(Uri uri);
}
.
.
Code language: Java (java)
Adding View Binding Support to the Destination Fragments
Since we will access some views in the fragment layouts, we must modify the current code to enable view binding support. Begin by editing the FirstFragment.java file and making the following changes:
.
.
import com.ebookfrenzy.navigationdemo.databinding.FragmentFirstBinding;
.
.
public class FirstFragment extends Fragment {
private FragmentFirstBinding binding;
.
.
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// return inflater.inflate(R.layout.fragment_first, container, false);
binding = FragmentFirstBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
.
.
Code language: Java (java)
Repeat the above steps for the SecondFragment.java file, referencing FragmentSecondBinding.
Triggering the Action
Now that the action has been added to the navigation graph, the next step is to add some code within the first fragment to trigger the action when the Button widget is clicked. Locate the FirstFragment.java file, load it into the code editor, and override the onViewCreated() method to obtain a reference to the button instance and to configure an onClickListener instance to be called when the user clicks the button:
.
.
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.widget.Button;
import androidx.navigation.Navigation;
.
.
public class FirstFragment extends Fragment {
.
.
@Override
public void onViewCreated(@NonNull View view,
@Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Navigation.findNavController(view).navigate(
R.id.mainToSecond);
}
});
}
}
Code language: Java (java)
The above code obtains a reference to the navigation controller and calls the navigate() method on that instance, passing through the resource ID of the navigation action as an argument.
Compile and run the app and verify that clicking the button in the first fragment transitions to the second fragment.
As an alternative to this approach to setting up a listener, the Navigation class also includes a method named createNavigateOnClickListener() which provides a more efficient way of setting up a listener and navigating to a destination. The same result can be achieved, therefore, using the following single line of code to initiate the transition:
binding.button.setOnClickListener(Navigation.createNavigateOnClickListener(
R.id.mainToSecond, null));
Code language: Java (java)
Passing Data Using Safeargs
The next objective is to pass the text entered into the EditText view in the first fragment to the second fragment, where it will be displayed on the TextView widget. As outlined in the previous chapter, the Android Navigation component supports two approaches to passing data. This chapter will make use of type-safe argument passing.
The first step in using safeargs is to add the safeargs plugin to the Gradle build configuration. Using the Project tool window, locate and edit the project-level build.gradle.kts file (Gradle Scripts -> build.gradle.kts (Project: NavigationDemo)) to add the plugin dependency as follows (once again, keeping in mind that a more recent version may now be available):
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
dependencies {
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.6.0")
}
}
plugins {
.
.
Code language: Gradle (gradle)
Next, edit the module level build.gradle.kts file (Gradle Scripts -> build.gradle.kts (Module :app)) to apply the plugin as follows and resync the project:
plugins {
id("com.android.application")
id ("androidx.navigation.safeargs")
.
.
android {
.
.
Code language: Gradle (gradle)
The next step is to define any arguments that will be received by the destination, which, in this case, is the second fragment. Edit the navigation graph, select the secondFragment destination, and locate the Arguments section within the Attributes tool window. Click on the + button (highlighted in Figure 41-14) to add a new argument to the destination:
After the + button has been clicked, a dialog will appear into which the argument name, type, and default value need to be entered. Name the argument message, set the type to String, enter No Message into the default value field, and click the Add button:
The newly configured argument will appear in the secondFragment element of the navigation_graph.xml file as follows:
<fragment
android:id="@+id/secondFragment"
android:name="com.ebookfrenzy.navigationdemo.SecondFragment"
android:label="fragment_second"
tools:layout="@layout/fragment_second" >
<argument
android:name="message"
app:argType="string"
android:defaultValue="No Message" />
</fragment>
Code language: HTML, XML (xml)
The next step is to add code to the FirstFragment.java file to extract the text from the EditText view and pass it to the second fragment during the navigation action. This will involve using some special navigation classes generated automatically by the safeargs plugin. Currently, the navigation involves the FirstFragment class, the SecondFragment class, a navigation action named mainToSecond, and an argument named message.
When the project is built, the safeargs plugin will generate the following additional classes that can be used to pass and receive arguments during navigation.
- FirstFragmentDirections – This class represents the origin for the navigation action (named using the class name of the navigation origin with “Directions” appended to the end) and provides access to the action object.
- ActionMainToSecond – The class representing the action used to perform the transition (named based on the ID assigned to the action within the navigation graph file prefixed with “Action”). This class contains a setter method for each argument configured on the destination. For example, since the second fragment destination contains an argument named message, the class includes a method named setMessage(). Once configured, an instance of this class is then passed to the navigate() method of the navigation controller to navigate to the destination.
- SecondFragmentArgs – The class used in the destination fragment to access the arguments passed from the origin (named using the class name of the navigation destination with “Args” appended to the end). This class includes a getter method for each of the arguments passed to the destination (i.e., getMessage())
Using these classes, the onClickListener code within the onViewCreated() method of the FirstFragment.java file can be modified as follows to extract the current text from the EditText widget, apply it to the action and initiate the transition to the second fragment:
.
.
binding.button.setOnClickListener(view1 -> {
FirstFragmentDirections.MainToSecond action =
FirstFragmentDirections.mainToSecond();
action.setMessage(binding.userText.getText().toString());
Navigation.findNavController(view1).navigate(action);
});
Code language: Java (java)
The above code obtains a reference to the action object, sets the message argument string using the setMessage() method, and then calls the navigate() method of the navigation controller, passing through the action object. If Android Studio reports FirstFragmentDirections as undefined, rebuild the project using the Build -> Make Project menu option to generate the class.
All that remains is to modify the SecondFragment.java class file to receive the argument after the navigation has been performed and display it on the TextView widget. For this example, the code to achieve these tasks will be added using an onStart() lifecycle method. Edit the SecondFragment.java file and add this method so that it reads as follows:
.
.
@Override
public void onStart() {
super.onStart();
SecondFragmentArgs args = SecondFragmentArgs.fromBundle(getArguments());
String message = args.getMessage();
binding.argText.setText(message);
}
Code language: Java (java)
The code in the above method begins by obtaining a reference to the TextView widget. Next, the fromBundle() method of the SecondFragmentArgs class is called to extract the SecondFragmentArgs object received from the origin. Since the argument in this example was named message in the navigation_graph.xml file, the corresponding getMessage() method is called on the args object to obtain the string value. This string is then displayed on the TextView widget.
Compile and run the app and enter some text before clicking on the Button widget. When the second fragment destination appears, the TextView should display the text entered in the first fragment, indicating that the data was successfully passed between navigation destinations.
Summary
This chapter has provided a practical example of implementing Android app navigation using the Navigation Architecture Component and the Android Studio navigation graph editor. Topics covered included the creation of a navigation graph containing both existing and new destination fragments, embedding a navigation host fragment within an activity layout, writing code to trigger navigation events, and passing arguments between destinations using the safeargs plugin.