preface
Some time ago, I encountered some small problems with Fragment management when I was doing project development. I always felt that the packaged Fragment manager was not elegant at the present stage. This led me to decide to learn Jetpack’s Navigation library, which was launched a long time ago to manage fragments more elegantly. When learning new knowledge, I prefer to write down my knowledge points and difficulties on paper. But sometimes because of the tight time, things written down on paper are often not so detailed and specific. So I decided to make a summary of the knowledge by writing a blog in the future, and ALSO hope to help you reading this blog. Let’s improve the Android knowledge system together!
Conditions of use
If you want to use the Navigation component in Android Studio, you must use Android Studio 3.3 or higher.
Add the dependent
To add Navigation support to your project, add the following dependencies to your application’s build.gradle file:
dependencies {
//...
implementation "Androidx. Navigation: navigation - fragments - KTX: 2.2.1." "
implementation "Androidx. Navigation: navigation - UI - KTX: 2.2.1." "
}
Copy the code
The specific process of using Navigation
Project presentations
Create three new FramgNets
Before configuring Navigation, we create three FramgNets for testing. The code looks like this:
// first Fragment class Page1Fragment:Fragment() { override fun onCreateView(inflater: LayoutInflater,container: ViewGroup? ,savedInstanceState: Bundle?) : View? {return inflater.inflate(R.layout.fragment_page_1, container, false}} // Second Framgnet class Page2Fragment:Fragment() { override fun onCreateView(inflater: LayoutInflater,container: ViewGroup? ,savedInstanceState: Bundle?) : View? {return inflater.inflate(R.layout.fragment_page_2, container, false}} // check whether Page3Fragment is displayed.Fragment() { override fun onCreateView(inflater: LayoutInflater,container: ViewGroup? ,savedInstanceState: Bundle?) : View? {return inflater.inflate(R.layout.fragment_page_3, container, false)}}Copy the code
Configuration navigation
- We need to create a new Navigaton folder under the RES folder.
- Create a Navigation resource file in the Navigaton folder.
- We’ll call it mobile_navigaion.xml.
As shown below:
You can either nest another
.
Add the three fragments we just created to mobile_navigation.xml.
The code looks like this:
<? 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">
<fragment
android:id="@+id/fragment_page_1_id"
android:name="com.johnlion.navigation.Page1Fragment"
android:label="fragment_page_1_label"
tools:layout="@layout/fragment_page_1" />
<fragment
android:id="@+id/fragment_page_2_id"
android:name="com.johnlion.navigation.Page2Fragment"
android:label="fragment_page_2_label"
tools:layout="@layout/fragment_page_2" />
<fragment
android:id="@+id/fragment_page_3_id"
android:name="com.johnlion.navigation.Page3Fragment"
android:label="fragment_page_1_label"
tools:layout="@layout/fragment_page_3" />
</navigation>
Copy the code
In the mobile_navigation. XML Fragment we just added, the
We open the Activity layout file and add a NavHostFragment to the layout file. The code looks like this:
<? 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">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code
Please note the following points:
The compiler does not provide intelligent prompts for NavHostFragment file path, defaultNavHost, and navGraph properties when adding NavHostFragment. Please do not panic, do not doubt whether there are these attributes or values, firmly and accurately knock it out or copy and paste success!
android:name
Property contains the class name of the NavHost implementation.app:navGraph
Property to associate a NavHostFragment with a navigation diagram. The navigation diagram specifies all the destinations the user can navigate to in this NavHostFragment.app:defaultNavHost="true"
Property to ensure that your NavHostFragment intercepts the system return button. Note that there can only be one default NavHost. If there are multiple hosts in the same layout (for example, a two-pane layout), be sure to specify only one default NavHost.
NavHostFragment is simply a navigation interface container that displays a series of fragments in navigation.
After NavHostFragment is added to the Activity layout file, the red warning of the navigation TAB in mobile_navigation. XML is replaced by a yellow alert. This is because we did not add a “start destination” in the navigation tag! So we open the first page of the app. In the
app:startDestination="@id/fragment_page_1_id"
Now in Navigation Edit, a small house is added to the top of the Fragment as the starting destination. As shown below:
So the Page1Fragment we created will be the first screen we display when we open the app.
After the “start destination” is configured, we need to configure the “destination” for each fragment. There are two ways to configure the “destination” : one is to drag the visual Fragment arrow in Navigation Edit to point to its “destination”; The second is to add the
tag to each Fragment and configure its “destination” directly in the XML. The code looks like this:
<? 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/fragment_page_1_id">
<fragment
android:id="@+id/fragment_page_1_id"
android:name="com.johnlion.navigation.Page1Fragment"
android:label="fragment_page_1_label"
tools:layout="@layout/fragment_page_1">
<action
android:id="@+id/action_page_1_to_page_2"
app:destination="@id/fragment_page_2_id" />
</fragment>
<fragment
android:id="@+id/fragment_page_2_id"
android:name="com.johnlion.navigation.Page2Fragment"
android:label="fragment_page_2_label"
tools:layout="@layout/fragment_page_2">
<action
android:id="@+id/action_page_2_to_page_3"
app:destination="@id/fragment_page_3_id" />
<action
android:id="@+id/action_page_2_to_page_1"
app:popUpTo="@id/fragment_page_1_id" />
</fragment>
<fragment
android:id="@+id/fragment_page_3_id"
android:name="com.johnlion.navigation.Page3Fragment"
android:label="fragment_page_1_label"
tools:layout="@layout/fragment_page_3">
<action
android:id="@+id/action_page_3_to_page_2"
app:popUpTo="@id/fragment_page_2_id" />
</fragment>
</navigation>
Copy the code
The application navigation diagram is as follows:
Realize the jump
Next we configure the corresponding jump event for each Fragment. The code looks like this:
// first Fragment class Page1Fragment:Fragment() { override fun onCreateView(inflater: LayoutInflater,container: ViewGroup? ,savedInstanceState: Bundle?) : View? {return inflater.inflate(R.layout.fragment_page_1, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) btn_page1.setOnClickListener { Navigation. FindNavController (it). Navigate (da ction_page_1_to_page_2 R.i)}}} / / the second Fragment class Page2Fragment:Fragment() { override fun onCreateView(inflater: LayoutInflater,container: ViewGroup? ,savedInstanceState: Bundle?) : View? {return inflater.inflate(R.layout.fragment_page_2, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) btn_page2_1.setOnClickListener { Navigation.findNavController(it).navigateUp() } btn_page2_2.setOnClickListener { Navigation.findNavController(it).navigate(R.id.action_page_2_to_page_3) } } } Class Page3Fragment:Fragment() { override fun onCreateView(inflater: LayoutInflater,container: ViewGroup? ,savedInstanceState: Bundle?) : View? {return inflater.inflate(R.layout.fragment_page_3, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_page3.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_page_3_to_page_2)
}
}
}
Copy the code
The API used for jumping between fragments is:
- Navigation. FindNavController (view). Navigate (actionID).
- Navigation. FindNavController (view). NavigateUp ().
Use the Bundle to pass parameters
The code looks like this:
btn_bundle.setOnClickListener {
val bundle = Bundle()
bundle.putString("key"."value")
Navigation.findNavController(it).navigate(R.id.action_page_1_to_page_2, bundle)
}
Copy the code
Insert the key: value to be passed into the Bundle by creating a Bundle() object, then our navigate(…) Method, so that we can pass parameters through the bundle.
Interface switching animation
We can animate transitions between destinations to make switching from Fragment to Fragment easier. So let’s try to animate right in and left out.
Create an ANim folder in the res directory to store the XML file that implements the animation. Then create two animation resource files named slide_in_right. XML and slide_out_left. XML, as shown below:
//slide_in_right.xml <? xml version="1.0" encoding="utf-8"? > <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100%"
android:interpolator="@android:anim/accelerate_interpolator"
android:toXDelta="0" />
</set> //slide_out_left <? xml version="1.0" encoding="utf-8"? > <set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0"
android:interpolator="@android:anim/accelerate_interpolator"
android:toXDelta="100%"/>
</set>
Copy the code
After the animation file is added, you can configure the animation transition between fragments in two ways. One is under the < Action > TAB in the navigation, and the other is through the navOptions method in the code. The animation configuration code is as follows:
<action Android :id= // We animate one Fragment to the second Fragment"@+id/action_page_1_to_page_2"
app:destination="@id/fragment_page_2_id"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"Val option = navOptions {anim {enter = r.nim.slide_in_rightexit = R.anim.slide_out_left
}
}
btn_page1.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_page_1_to_page_2, null, option)
}
Copy the code
Navigate: navigate(…) The first argument is passed in the actionID, the second argument is passed in the Bundle object, and the third argument is passed in the navOptions, since we are not creating a bundle.
This completes the animation of our transition between destinations
At this point, by running the preview, you have basically the same effect as the example
Use Safe Args to deliver secure data
The Navigation component has a Gradle plug-in called Safe Args that generates simple Object and Builder classes for browsing and accessing any associated parameters in a type-safe manner. We strongly recommend that you use Safe Args for navigation and data delivery because it ensures type safety.
Configure the security plug-in first.
Gradle buildScript {repositories {//... google() } dependencies { //... def nav_version ="2.1.0."
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"}} // Build. Gradle at application or module level // apply plugin:'com.android.application'
apply plugin: "androidx.navigation.safeargs.kotlin"
android{
//...
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
}
Copy the code
When the security plug-in is complete, it automatically generates a class with Directions at the end, the name of which is “Directions” after the name of the source destination, and the method for the action of the original destination.
As described in the official documentation, the Navigation library supports the following parameter types:
First we need to add our custom parameters to mobile_navigation. XML. If we want to pass the parameters from Page1 to Page2, Add the argument tag under the fragment tag in Page2 and add the name, default value, and type. The code looks like this:
<fragment
android:id="@+id/fragment_page_2_id"
android:name="com.johnlion.navigation.Page2Fragment"
android:label="fragment_page_2_label"
tools:layout="@layout/fragment_page_2"> <! -... --> <argument android:name="myInteger"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="myString"
android:defaultValue="value"
app:argType="string" />
<argument
android:name="myBoolean"
android:defaultValue="false"
app:argType="boolean" />
</fragment>
Copy the code
After adding, we must reBuild the project, so that the security plug-in will generate for us a class with “Args” at the end, which contains the method that we receive the parameter destination Page2 to get the parameter.
Page1FragemntDirections
class Page1FragmentDirections private constructor() {
//...
companion object {
fun actionPage1ToPage2(
myInteger: Int = 0,
myString: String = "value",
myBoolean: Boolean = false
): NavDirections = ActionPage1ToPage2(myInteger, myString, myBoolean)
}
}
Copy the code
Let’s take a look at how to pass secure data in code. The code looks like this:
// class Page1Fragment:Fragment() {
//...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_page1.setOnClickListener {
val action = Page1FragmentDirections.actionPage1ToPage2(1, "hello".true) Navigation. FindNavController (it). Navigate (action)}} / / Page2Fragment (receive safety data) class Page2Fragment:Fragment() { val args: Page2FragmentArgs by navArgs() override fun onViewCreated(view: View, savedInstanceState: Bundle?) {/ /... Log.d(log.d) = log.d (log.d)"data"."integer:" + args.myInteger)
Log.d("data"."string:" + args.myString)
Log.d("data"."boolean:" + args.myBoolean)
}
}
Copy the code
If we are in actionPage1ToPage2(…) If nothing is added to the method, android:defaultValue is passed in the XML setting.
The argument passed from Page1 to Page2 must not be null, but if the argument type supports null values, you can use
Use android:defaultValue=”@null” and app:nullable=”true” to declare the defaultValue null.
In general, safe Args gives me the feeling that they are used to pass data around so as to avoid null-pointer exceptions when receiving data!
Update interface components using NavigationUI
The navigation architecture component contains the NavigationUI class. This class contains static methods for managing navigation using the top application bar, the drawer navigation bar, and the bottom navigation bar.
NavigationUI supports the following types of controls:
- Toolbar
- CollapsingToolbarLayout
- ActionBar
- DrawerLayout
- BottomNavigationView
Let’s select BottomNavigationView with Navigation for an example:
- Create a new menu folder under the res directory.
- Create a Menu resource file in the Menu folder named menu.xml.
- Add the following code to this file:
Note: the id in item must match the Fragment ID you want to display in mobile_navigation.xml
<? xml version="1.0" encoding="utf-8"? > <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/fragment_page_1_id"
android:icon="@drawable/message"
android:title="Page1" />
<item
android:id="@+id/fragment_page_2_id"
android:icon="@drawable/search"
android:title="Page2" />
<item
android:id="@+id/fragment_page_3_id"
android:icon="@drawable/setting"
android:title="Page3" />
</menu>
Copy the code
We then add the BottomNavigationView control to the Activity layout file and associate the newly created Menu file with it. The code looks like this:
<? xml version="1.0" encoding="utf-8"? > <androidx.constraintlayout.widget.ConstraintLayout ... > <fragment android:id="@+id/nav_host_fragment". /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code
Finally, we need to extract the NavController from the activity code and then pass it into BottomNavigationView setupWithNavController(…). So the first step is to find the NavHostFragment first, then retrieve the NavController, and finally pass it to setupWithNavController(…). Method that’s done binding Navigation to BottomNavigationView. The code looks like this:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) val host: NavHostFragment = supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ? :returnval navController = host.navController setupBottomNavMenu(navController) } private fun setupBottomNavMenu(navController: NavController) { val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation) bottomNav? .setupWithNavController(navController) } }Copy the code
The demo effect is as follows:
But! If you click on the BottomNavigationView item, then click on Page2 -> Page3, and then stay in Page3. If you click on the back button on the real phone, you will jump back to Page1, and then click again to exit the application. Try it a few more times and you’ll get back to Page1 if you hit the back button and you’re not stuck in Page1.
What we want is: just click on BottomNavigation to select the Fragment, return to the stack with only one Fragment, and then click the Back button to exit the application.
Locating problems:
- Enter the
bottomNav? .setupWithNavController(navController)
In the method. - It turns out there’s only one
NavigationUI.setupWithNavController(this, navController)
Method and enter the method. - There’s one right thereBottomNavigationViewImplemented listening
setOnNavigationItemSelectedListener
One is returned inonNavDestinationSelected(item, navController);
This method is used toassociatedNavigation with BottomNavigationView MenuItem. - That’s where the problem comes from
setPopUpTo(int destinationId, boolean inclusive)
, the source code is as follows:
public static boolean onNavDestinationSelected(@NonNull MenuItem item,@NonNull NavController navController) {
NavOptions.Builder builder = new NavOptions.Builder()
//...
if ((item.getOrder() & Menu.CATEGORY_SECONDARY) == 0) {
builder.setPopUpTo(findStartDestination(navController.getGraph()).getId(), false);
}
NavOptions options = builder.build();
try {
navController.navigate(item.getItemId(), null, options);
return true;
} catch (IllegalArgumentException e) {
return false; }}Copy the code
Every time we click on the BottomNavigationView Item, we’re going to go setPopUpTo(…). The inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false” inclusive=”false”
That’s why: clicking the back button will return to Page1 first if you’re not stuck in Page1!
Solve a problem:
- The question comes from a man called
onNavDestinationSelected(item, navController);
In the method of. - The method only realized the navigation jump destination, and set the jump in and out animation and destination start mode.
- This method is in BottomNavigationView
setOnNavigationItemSelectedListener
Listen in. - The listener is implemented only
onNavDestinationSelected(item, navController)
One way!
That we can try to write BottomNavigationView setOnNavigationItemSelectedListener monitoring, according to onNavDestinationSelected (…). Method inside the content, modify the effect we need not on the line!
The activity code looks like this:
class MainActivity : AppCompatActivity() {/ /... private fun setupBottomNavMenu(navController: NavController) { val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation) bottomNav? . SetupWithNavController (navController) / / rewrite to monitor bottomNav setOnNavigationItemSelectedListener {item: MenuItem - > val options = NavOptions. Builder () / / removed from the back stack designated destination. SetPopUpTo (navController currentDestination!! .id,true)
.setLaunchSingleTop(true)
.build()
try {
//TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
true
} catch (e: IllegalArgumentException) {
false}}}}Copy the code
The demo effect is as follows:
Done! The test results are exactly what we want!
Dynamically loaded Navigation
Remove app:navGraph=”@navigation/mobile_navigation” from the Activity layout file. The code looks like this:
<? xml version="1.0" encoding="utf-8"? > <androidx.constraintlayout.widget.ConstraintLayout ... > <fragment android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code
Then in the Activity file, use the navController to feed the Navigation XML file to the Inflater and set it into the navController graph. The code looks like this:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) val host: NavHostFragment = supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ? :returnval navController = host.navController val navGraph: NavGraph = navController.navInflater.inflate(R.navigation.mobile_navigation) navController.graph = navGraph //... } / /... }Copy the code
Test again, dynamic loading successful!
Clear back stack
If we want to jump from Page2 to Page3, we should clear the back stack first, and then jump to Page3, then there should be only one instance of Page3 in the back stack. When we click back’, we will exit the application directly instead of putting Page2 back.
This is where I found a way to clear navigation back stack on StackOverflow.
The implementation code is as follows:
<? xml version="1.0" encoding="utf-8"? > <navigation xmlns:android="http://schemas.android.com/apk/res/android". > <! -... --> <fragment android:id="@+id/fragment_page_2_id". > <action android:id="@+id/action_page_2_to_page_3"
app:destination="@id/fragment_page_3_id"
app:launchSingleTop="true"
app:popUpTo="@+id/mobile_navigation"
app:popUpToInclusive="true" />
<!--
...
-->
</fragment>
</navigation>
Copy the code
Add app:popUpToInclusive=”true”, app:popUpTo=”@+ ID /mobile_navigation”, app:popUpToInclusive=”true”, app:popUpToInclusive=”true”, app:popUpToInclusive=”true” You can clear the stack and jump.
We tried to implement this in code based on these attributes added to the XML. The code looks like this:
class Page2Fragment : Fragment() {/ /... override fun onViewCreated(view: View, savedInstanceState: Bundle?) {/ /... Btn_page2_.setonclicklistener {val navOption = navoptions.builder () // Set the following properties to the btn_page2_.setonClickListener {val navOption = navoptions.builder ()true)
.setPopUpTo(R.id.mobile_navigation, true)
.build()
Navigation.findNavController(it).navigate(R.id.action_page_2_to_page_3, null, navOption)
}
}
}
Copy the code
Finally, implement a global stack clearing function in the Activity.
The code is very simple, take the NavController, Then the call navController. Navigate (XXX) method is called before navController. PopBackStack (R.i d.m obile_navigation, true) method can achieve the stack. The code looks like this:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) val host: NavHostFragment = supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? ? :return
val navController = host.navController
setupBottomNavMenu(navController)
}
private fun clearStack(navController: NavController) {
navController.popBackStack(R.id.mobile_navigation, true} / /... }Copy the code
Of course, “destination” between the jump, should be unified in the activity management, not detailed here.
As soon as you get your navController, you can call itnavController.navigate(xxx)
Go to the Destination jump.
conclusion
Android JetPack’s Navigation architecture component, which serves as a framework for building in-app interfaces, focuses on making single-activity apps the preferred architecture. This control handles the complexity of FragmentTransaction and provides helper programs for associating navigation with appropriate UI widgets, such as the drawer navigation bar and bottom navigation.
Project link
Example: the android – navigation
Refer to the article
- Navigation: a very awkward Fragment management framework
- Android Navigation Architecture Component
- The official documentation
- Official Project Address
- Official Project Tutorial