Android 官方架构组件 Navigation 使用详解,构建一本Fragment的故事书!

前言

前段时间,我在做项目开发的时候对Fragment的管理遇到几个小问题,总觉得在现阶段封装好的Fragment管理器不太优雅。这成为我下决心学习Jetpack在很早之前推出的Navigation库,该库的诞生就是为了能够更加优雅的管理Fragment。在学习新知识时,我比较喜欢将我遇到的知识点与难点写在纸上。但有时由于时间比较紧,记在纸上的东西往往没有那么的详细与具体。所以我决定以后通过以写博客的方式对知识进行一个归纳总结,也希望能帮助到在看这篇博客的你。让我们一起来完善Android的知识体系吧!

image

使用条件

如果您要在 Android Studio 中使用 Navigation 组件,则必须使用 Android Studio 3.3 或更高版本。

添加依赖

要在您的项目中添加 Navigation 支持,请向应用的 build.gradle 文件添加以下依赖项:
dependencies {
    //...
    implementation "androidx.navigation:navigation-fragment-ktx:2.2.1"
    implementation "androidx.navigation:navigation-ui-ktx:2.2.1"
}

使用Navigation的具体流程

项目演示

image

新建三个Framgnet

在配置Navigation之前,我们创建三个Framgnet用于测试。代码如下所示:

//第一个Fragment
class Page1Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_1, container, false)
    }
}
//第二个Framgnet
class Page2Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_2, container, false)
    }
}
//第三个Fragment
class Page3Fragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? 
    {
        return inflater.inflate(R.layout.fragment_page_3, container, false)
    }
}

配置导航

  • 我们需要在res文件夹下新建一个navigaton文件夹。
  • navigaton文件夹下创建一个navigation资源文件
  • 我们将它起名为mobile_navigaion.xml

如下图所示:


image

<navigation>标签里可以嵌套另一个<navigation>标签,也可以新建一个navigation xml文件,通过<include>将其引进来。

在mobile_navigation.xml中添加上我们刚创建好的三个Fragment。

<fragment>标签下要有id,name,labellayout,这四个属性一定要齐全!代码如下所示:

<?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>

此时我们刚添加完Fragment的mobile_navigation.xml中,<navigation>标签在android studio编译器中会报红色警告,这是由于我们并未在Activity布局文件中添加好NavHostFragment

我们打开Activity布局文件,并在布局文件中添加一个NavHostFragment。代码如下所示:

<?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>

请注意以下几点:

在添加NavHostFragment的时候编译器并未对NavHostFragment的文件路径、defaultNavHost与navGraph属性进行智能提示。请你不要慌,不要怀疑是否有这些属性或值,坚定准确的把它敲完或复制粘贴成功!
  • android:name 属性包含 NavHost 实现的类名称。
  • app:navGraph 属性将 NavHostFragment 与导航图相关联。导航图会在此 NavHostFragment 中指定用户可以导航到的所有目的地。
  • app:defaultNavHost="true" 属性确保您的 NavHostFragment 会拦截系统返回按钮。请注意,只能有一个默认 NavHost。如果同一布局(例如,双窗格布局)中有多个主机,请务必仅指定一个默认 NavHost。

NavHostFragment 简单来讲就是一个导航界面容器,用来展示导航中一系列的 Fragment。

NavHostFragment在Activity布局文件中添加成功后,mobile_navigation.xml中navigation标签的红色警告消失了,取而代之的是一个黄色的提醒,这是由于我们并未在navigation的标签中添加一个“起始目的地"!即我们打开应用的第一张页面。在<navigation>标签中添加上:

app:startDestination="@id/fragment_page_1_id"

此时在Navigation Edit中,作为起始目的地的Fragment顶部就会多了一个小房子。如下图所示:

image
这样我们创建好的Page1Fragment就会变成我们打开应用显示的第一个屏幕。

“起始目的地”配置好之后,我们需要为每一个fragment配置其的“目的地”。配置“目的地”的方式有两种,一种是在Navigation Edit中,拖拉可视化Fragment的箭头来指向它的“目的地”;第二种是直接在xml中,在每个Fragment中添加<action>标签,并配置好其“目的地”。代码如下所示:

<?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>
应用导航图如下图所示:
image

实现跳转

接下来我们给每个Fragment配置好其对应的跳转事件。代码如下所示:

//第一个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(R.id.action_page_1_to_page_2)
        }
    }
}
//第二个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)
        }
    }
}
//第三个Fragment
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)
        }
    }
}

从中我们可以看出Fragment之间的跳转用到的API为:

  • Navigation.findNavController(view).navigate(actionID)
  • Navigation.findNavController(view).navigateUp()

使用Bundle传递参数

代码如下所示:

btn_bundle.setOnClickListener {
            val bundle = Bundle()
            bundle.putString("key", "value")
            Navigation.findNavController(it).navigate(R.id.action_page_1_to_page_2, bundle)
        }

通过创建一个Bundle()对象,把所要传递的key:value放进bundle里面,然后我们在的navigate(...)方法中把bundle传进去,这样我们就可以通过bundle来实现参数的传递了。

界面切换动画

我们可以在目的地之间添加上动画的过渡效果,使Fragment与Fragment之前切换不生硬。那么我们来尝试做个右进左出的动画吧。

我们先创建所需要的xml文件,在res目录下新建一个anim文件夹,用来存放实现动画的xml文件,然后新建两个动画资源文件,起名为slide_in_right.xmlslide_out_left.xml,代码如下所示:

//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>

添加动画文件完成之后我们可以通过两种方法来配置Fragment与Fragment之间跳转的动画过渡,一种是在导航中的<action>标签下配置,另一种是通过代码中的navOptions方法进行配置,动画配置代码如下所示:

//我们对一张Fragment跳转到第二张Fragment进行动画配置
//第一种:xml静态配置
    <action
        android:id="@+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.anim.slide_in_right
            exit = R.anim.slide_out_left
            }
    }
    btn_page1.setOnClickListener {
        Navigation.findNavController(it).navigate(R.id.action_page_1_to_page_2, null, option)
    }

在代码中配置需要注意navigate(...)中第一个参数传入的是actionID,第二个参数传入的是bundle对象,由于我们并没有新建一个bundle,所以选择传入null,第三个参数则是navOptions

这样我们目的地与目的地之间的过渡动画就添加完成了~

image
到这里,通过运行预览,基本和示例一样效果了

使用 Safe Args 传递安全的数据

官方文档原话:
Navigation 组件具有一个名为 Safe Args 的 Gradle 插件,该插件可以生成简单的 object 和 builder 类,以便以类型安全的方式浏览和访问任何关联的参数。我们强烈建议您将 Safe Args 用于导航和数据传递,因为它可以确保类型安全。

先配置安全插件。

//顶级的build.gradle
    buildscript {
        repositories {
            //...
            google()
        }
        dependencies {
            //...
            def nav_version = "2.1.0"
            classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
        }
    }
//应用或模块级的build.gradle
    //...
    apply plugin: 'com.android.application'
    apply plugin: "androidx.navigation.safeargs.kotlin"
    android{
        //...
        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_1_8
        }   
    }

添加安全插件完成后,它会自动生成末尾带有Directions的类,该类的名称是在源目的地的名称后面加上“Directions”,该类里面带有原目的地action的方法。

image

在官方文档中介绍,Navigation 库支持以下参数类型:
image

那我们来挑上几种类型尝试一遍吧。

首先我们需要在mobile_navigation.xml中添加上我们自定义的参数,假如我们打算从Page1中把参数传递给Page2,那么我们则需要在Page2(接收目的地)的<fragment>标签下添加上<argument>标签并添加上名字默认值类型三种属性。代码如下所示:

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

添加完成后一定要reBuild一下项目,这样安全插件会为我们生成末尾带有“Args”的类,里面有我们接收参数目的地即Page2获取参数的方法。

image

而且在Page1FragemntDirections类中,用来实现action的方法在reBuild后会更新成传递三个默认参数给Page2接收。代码如下所示:

class Page1FragmentDirections private constructor() {
    //...
    companion object {
        fun actionPage1ToPage2(
            myInteger: Int = 0,
            myString: String = "value",
            myBoolean: Boolean = false
        ): NavDirections = ActionPage1ToPage2(myInteger, myString, myBoolean)
    }
}

接下来我们看下如何在代码中传递安全的数据。代码如下所示:

//Page1Fragment(传递安全数据)
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(接收安全数据)
class Page2Fragment : Fragment() {
    val args: Page2FragmentArgs by navArgs()
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        //...
        //获取的参数名为<argument>中配置好的name
        Log.d("data", "integer:" + args.myInteger)
        Log.d("data", "string:" + args.myString)
        Log.d("data", "boolean:" + args.myBoolean)
    }
}

如果我们在actionPage1ToPage2(...)方法里什么都不加的话,就会把在xml设置好的android:defaultValue传递过去。

从Page1传递Page2的参数要求不能为null,但如果参数类型支持 null 值,那么可以在<argument>中,使用 android:defaultValue="@null"app:nullable="true"结合声明默认值 null。

总的来讲,safe args给我的感觉就是,通过其用来传递数据,就是为了避免接收数据时会报空指针异常

使用 NavigationUI 更新界面组件

导航架构组件包含 NavigationUI 类。此类包含使用顶部应用栏、抽屉式导航栏和底部导航栏管理导航的静态方法。

NavigationUI支持以下类型控件:

  • Toolbar
  • CollapsingToolbarLayout
  • ActionBar
  • DrawerLayout
  • BottomNavigationView

我们选择BottomNavigationView结合Navigation做一个示例:

  • 在res目录下下新建一个menu文件夹。
  • 在menu文件夹中创建一个menu资源文件起名为:menu.xml
  • 在此文件中添加上如下代码:
注意:item中的id必须要与在mobile_navigation.xml中你要显示的Fragment id一致
<?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>

然后我们在Activity布局文件中添加BottomNavigationView控件,并把刚刚新创建好的menu文件关联其中。代码如下所示:

<?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>

最后我们在activity的代码中需要先把NavController给取出来,再把它传进BottomNavigationView中的setupWithNavController(...)方法,而NavController是要在NavHostFragment里面取,所以第一步就是要把NavHostFragment找出来先,然后取出NavController,最后把它传递到setupWithNavController(...)方法里就完成Navigation与BottomNavigationView的绑定了。代码如下所示:

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 setupBottomNavMenu(navController: NavController) {
        val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
        bottomNav?.setupWithNavController(navController)
    }
}

演示效果如下:

image

但是!看演示的效果会发现,我们依次点击BottomNavigationView上面的item,Page2 -> Page3,然后停留在Page3中点击真机上面的back按键则会先跳回到Page1,再点一次才退出应用,再试几遍才发现只要你不是停留在Page1中点击back按键都会先返回到Page1中,再点击一次back按键才退出应用

我们要的效果是:只要点击BottomNavigation选择的Fragment,返回栈中只留它一个,再点击back按钮就可退出应用。
image

定位问题:

  • 进入bottomNav?.setupWithNavController(navController)方法里。
  • 发现里面只有一个NavigationUI.setupWithNavController(this, navController)方法,并进入该方法。
  • 里面有一个对BottomNavigationView实现的监听setOnNavigationItemSelectedListener里返回了一个onNavDestinationSelected(item, navController);,这个方法是用来关联Navigation与BottomNavigationView的MenuItem的。
  • 问题就是出自这个方法里的setPopUpTo(int destinationId, boolean inclusive),源码如下所示:
 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;
        }
    }

当我们每次点击BottomNavigationView的Item的时候,都会走setPopUpTo(...)方法,由于里面只指定“初始目的地”的id,所以每次都会弹出在其之上的目的地,且inclusive="false",这样“初始目的地”在栈中得到保留,并没有被移除!

这就是为什么:只要你不是停留在Page1中点击back按键都会先返回到Page1!

image

解决问题:

  • 问题出自一个叫onNavDestinationSelected(item, navController);的方法里。
  • 该方法里面只实现了导航跳转目的地,与设置跳转时的进出动画和目的地的启动模式
  • 此方法在BottomNavigationView的setOnNavigationItemSelectedListener监听里面。
  • 该监听只实现onNavDestinationSelected(item, navController)一个方法!

那我们可以试着重写BottomNavigationView的setOnNavigationItemSelectedListener监听,照着onNavDestinationSelected(...)方法里面的内容,修改成我们需要的效果不就行了!

activity代码如下所示:

class MainActivity : AppCompatActivity() {
    //...
    private fun setupBottomNavMenu(navController: NavController) {
        val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_navigation)
        bottomNav?.setupWithNavController(navController)
        //重写监听
        bottomNav.setOnNavigationItemSelectedListener { item: MenuItem ->
            val options = NavOptions.Builder()
                //从放回栈中移除指定目的地
                .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
            }
        }
    }
}

演示效果如下:

image
完成!测试结果与我们想要的效果一致!

动态加载Navigation

先把Activity布局文件中的app:navGraph="@navigation/mobile_navigation"去掉先。代码如下所示:

<?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>

接着在Activity文件中,通过navController将Navigation的xml文件给inflater出来,并设置进navControllergraph中。代码如下所示:

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
        val navGraph: NavGraph =
            navController.navInflater.inflate(R.navigation.mobile_navigation)
        navController.graph = navGraph
        //...
    }
    //...
}

测试一遍,动态加载成功!

清空返回栈

假如我们打算从Page2跳到Page3时先把返回栈清空,然后跳转到Page3,这时返回栈应该就只有一个Page3的实例,当我们点击back'按键后,直接退出应用而不是放回Page2。

这是我在stackoverflow上找到了清空navigation返回栈的方法

实现代码如下所示:

<?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>

在Page2跳转到Page3的<action>标签下添加上app:launchSingleTop="true"app:popUpTo="@+id/mobile_navigation"app:popUpToInclusive="true"就能实现先清空放回栈然后跳转

我们根据xml中添加的这些属性尝试在代码中实现。代码如下所示:

class Page2Fragment : Fragment() {
    //...
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        //...
        btn_page2_2.setOnClickListener {
            val navOption = NavOptions.Builder()
                //将xml中三个属性设置进去,就能实现先清栈后跳转
                .setLaunchSingleTop(true)
                .setPopUpTo(R.id.mobile_navigation, true)
                .build()
            Navigation.findNavController(it).navigate(R.id.action_page_2_to_page_3, null, navOption)
        }
    }
}
最后在Activity中实现一个全局清栈功能。

代码很简单,取出NavController,然后在调用navController.navigate(xxx)方法前调用navController.popBackStack(R.id.mobile_navigation, true)方法就能实现清栈。代码如下所示:

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

当然“目的地”之间的跳转,应该统一放在activity里面管理,这里就不详细说明了。

只要拿到了navController,就能调用navController.navigate(xxx)进行“目的地”跳转。

总结

Android JetPack推出的Navigation架构组件,用来作为构建应用内界面的框架,其重点是让单Activity应用成为首选架构。此控件处理了FragmentTransaction 的复杂性,并提供了帮助程序,用于将导航关联到合适的 UI 小部件,例如抽屉式导航栏和底部导航。

image

项目链接

示例:android-navigation

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容