Android Navigation

image.png

在JetPack中有一个组件是Navigation,顾名思义它是一个页面导航组件,相对于其他的第三方导航,不同的是它是专门为Fragment的页面管理所设计的。它对于单个Activity的App来说非常有用,因为以一个Activity为架构的App页面的呈现都是通过不同的Fragment来展示的。所以对于Fragment的管理至关重要。通常的实现都要自己维护Fragment之间的栈关系,同时要对Fragment的Transaction操作非常熟悉。为了降低使用与维护成本,所以就有了今天的主角Navigation。

如果你对JetPack的其它组件感兴趣,推荐你阅读我之前的系列文章,本篇文章目前为JetPack系列的最后一篇。

Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle
Android Architecture Components Part4:ViewModel
Paging在RecyclerView中的应用,有这一篇就够了
WorkManager从入门到实践,有这一篇就够了

对于Navigation的使用,我将其归纳于以下四点:

  • Navigation的基本配置
  • Navigation的跳转与数据传递
  • Navigation的页面动画
  • Navigation的deepLink

配置


在使用之前需要引入Navigation的依赖,然后我们需要为Navigation创建一个配置文件,它将位于res/navigation/nav_graph.xml。为了方便理解文章中的代码,我写了一个Demo,大家可以通过Android精华录查看。

在我的Demo中打开nav_graph.xml你将清晰的看到它们页面间的关系纽带


image.png

一共有6个页面,最左边的为程序入口页面,它们间的线条指向为它们间可跳转的方向。
我们再来看它们的xm配置👇

<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/nav_graph"
    app:startDestination="@id/welcome_fragment">
 
    <fragment
        android:id="@+id/welcome_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment"
        android:label="welcome_fragment"
        tools:layout="@layout/fragment_welcome">
 
        <action
            android:id="@+id/action_go_to_register_page"
            app:destination="@id/register_fragment" />
 
        <action
            android:id="@+id/action_go_to_order_list_page"
            app:destination="@id/order_list_fragment"/>
 
    </fragment>
 
    <fragment
        android:id="@+id/register_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment"
        android:label="register_fragment"
        tools:layout="@layout/fragment_register">
 
        <action
            android:id="@+id/action_go_to_shop_list_page"
            app:destination="@id/shop_list_fragment" />
 
    </fragment>
     
    ...
</navigation>

页面标签主要包含navigation、fragment与action

  • navigation: 定义导航栈,可以进行嵌套定义,各个navigation相互独立。它有一个属性startDestination用来定义导航栈的根入口fragment
  • fragment: 顾名思义fragment页面。通过name属性来定义关联的fragment
  • action: 意图,可以理解为Intent,即跳转的行为。通过destination来关联将要跳转的目标fragment。

以上是nav_graph.xml的基本配置。
在配置完之后,我们还需要将其关联到Activity中。因为所有的Fragment都离不开Activity。
Navigation为我们提供了两个配置参数: defaultNavHost与navGraph,所以在Activity的xml中需要如下配置👇

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:background="@android:color/background_light"
    android:orientation="vertical"
    tools:context=".navigation.NavigationMainActivity">
 
    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_graph" />
 
</LinearLayout>
  • defaultNavHost: 将设备的回退操作进行拦截,并将其交给Navigation进行管理。
  • navGraph: Navigation的配置文件,即上面我们配置的nav_graph.xml文件

除此之外,fragment的name属性必须为NavHostFragment,因为它会作为我们配置的所有fragment的管理者。具体通过内部的NavController中的NavigationProvider来获取Navigator抽象实例,具体实现类是FragmentNavigator,所以最终通过它的navigate方法进行创建我们配置的Fragment,并且添加到NavHostFragment的FrameLayout根布局中。
此时如果我们直接运行程序后发现已经可以看到入口页面WelcomeFragment


image.png

但点击register等操作你会发现点击跳转无效,所以接下来我们需要为其添加跳转

跳转


由于我们之前已经在nav_graph.xml中定义了action,所以跳转的接入非常方便,每一个action的关联跳转只需一行代码👇

class WelcomeFragment : Fragment() {
 
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.fragment_welcome, container, false).apply {
            register_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_register_page))
            stroll_bt.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_go_to_order_list_page))
        }
    }
}

代码中的id就是配置的action的id,内部原理是先获取到对应的NavController,通过点击的view来遍历找到最外层的parent view,因为最外层的parent view会在配置文件导入时,即NavHostFragment中的onViewCreated方法中进行关联对应的NavController👇

  @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (!(view instanceof ViewGroup)) {
            throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
        }
        Navigation.setViewNavController(view, mNavController);
        // When added programmatically, we need to set the NavController on the parent - i.e.,
        // the View that has the ID matching this NavHostFragment.
        if (view.getParent() != null) {
            View rootView = (View) view.getParent();
            if (rootView.getId() == getId()) {
                Navigation.setViewNavController(rootView, mNavController);
            }
        }
    }

然后再调用navigate进行页面跳转处理,最终通过FragmentTransaction的replace进行Fragment替换👇

 -------------- NavController ------------------
     
    private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        boolean popped = false;
        if (navOptions != null) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        # ---- 关键代码 -------
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        ....
    }
     
    -------------- FragmentNavigator ------------------
 
    public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
        if (mFragmentManager.isStateSaved()) {
            Log.i(TAG, "Ignoring navigate() call: FragmentManager has already"
                    + " saved its state");
            return null;
        }
        String className = destination.getClassName();
        if (className.charAt(0) == '.') {
            className = mContext.getPackageName() + className;
        }
        final Fragment frag = instantiateFragment(mContext, mFragmentManager,
                className, args);
        frag.setArguments(args);
        final FragmentTransaction ft = mFragmentManager.beginTransaction();
 
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }
 
        # ------ 关键代码 ------
        ft.replace(mContainerId, frag);
        ft.setPrimaryNavigationFragment(frag);
        ...
    }

源码就分析到这里了,如果需要深入了解,建议阅读NavHostFragment、NavController、NavigatorProvider与FragmentNavigator

传参


以上是页面的无参跳转,那么对于有参跳转又该如何呢?
大家想到的应该都是bundle,将传递的数据填入到bundle中。没错Navigator提供的navigate方法可以进行传递bundle数据👇

findNavController().navigate(R.id.action_go_to_shop_detail_page, bundleOf("title" to "I am title"))

这种传统的方法在传递数据类型上并不能保证其一致性,为了减少人为精力上的错误,Navigation提供了一个Gradle插件,专门用来保证数据的类型安全。
使用它的话需要引入该插件,方式如下👇

buildscript {
    repositories {
        google()
    }
    dependencies {
        def nav_version = "2.1.0"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }
}

最后再到app下的build.gradle中引入该插件👇

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

而它的使用方式也很简单,首先参数需要在nav_graph.xml中进行配置。👇

  <fragment
        android:id="@+id/shop_list_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopListFragment"
        android:label="shop_list_fragment"
        tools:layout="@layout/fragment_shop_list">
 
        <action
            android:id="@+id/action_go_to_shop_detail_page"
            app:destination="@id/shop_detail_fragment">
 
            <argument
                android:name="title"
                app:argType="string" />
 
        </action>
 
    </fragment>
 
    <fragment
        android:id="@+id/shop_detail_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.ShopDetailFragment"
        android:label="shop_detail_fragment"
        tools:layout="@layout/fragment_shop_detail">
 
        <action
            android:id="@+id/action_go_to_cart_page"
            app:destination="@id/cart_fragment"
            app:popUpTo="@id/cart_fragment"
            app:popUpToInclusive="true" />
 
        <argument
            android:name="title"
            app:argType="string" />
 
    </fragment>

现在我们从ShopListFragment跳转到ShopDetailFragment,需要在ShopListFragment的对应action中添加argument,声明对应的参数类型与参数名,也可以通过defaultValue定义参数的默认值与nullable标明是否可空。对应的ShopDetailFragment接收参数也是一样。
另外popUpTo与popUpToInclusive属性是为了实现跳转到CartFragment时达到SingleTop效果。
下面我们直接看在代码中如何使用这些配置的参数,首先是在ShopListFragment中👇

holder.item.setOnClickListener(Navigation.createNavigateOnClickListener(ShopListFragmentDirections.actionGoToShopDetailPage(shopList[position])))

还是创建一个createNavigateOnClickListener,只不过现在传递的不再是跳转的action id,而是通过插件自动生成的ShopListFragmentDirections.actionGoToShopDetailPage方法。一旦我们如上配置了argument,插件就会自动生成一个以[类名]+Directions的类,而自动生成的类本质是做了跳转与参数的封装,源码如下👇

class ShopListFragmentDirections private constructor() {
    private data class ActionGoToShopDetailPage(val title: String) : NavDirections {
        override fun getActionId(): Int = R.id.action_go_to_shop_detail_page
 
        override fun getArguments(): Bundle {
            val result = Bundle()
            result.putString("title", this.title)
            return result
        }
    }
 
    companion object {
        fun actionGoToShopDetailPage(title: String): NavDirections = ActionGoToShopDetailPage(title)
    }
}

通过navArgs来获取ShopDetailFragmentArgs对象,它其中包含了传递过来的页面数据。

动画


在action中不仅可以配置跳转的destination,还可以定义对应页面的转场动画,使用非常简单👇

<?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/nav_graph"
    app:startDestination="@id/welcome_fragment">
 
    <fragment
        android:id="@+id/welcome_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.WelcomeFragment"
        android:label="welcome_fragment"
        tools:layout="@layout/fragment_welcome">
 
        <action
            android:id="@+id/action_go_to_register_page"
            app:destination="@id/register_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_in_left"
            app:popEnterAnim="@anim/slide_out_left"
            app:popExitAnim="@anim/slide_out_right" />
 
        <action
            android:id="@+id/action_go_to_order_list_page"
            app:destination="@id/order_list_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_in_left"
            app:popEnterAnim="@anim/slide_out_left"
            app:popExitAnim="@anim/slide_out_right" />
 
    </fragment>
    ...
</navigation>

对应四个动画配置参数

  • enterAnim: 配置进场时目标页面动画
  • exitAnim: 配置进场时原页面动画
  • popEnterAnim: 配置回退pop时目标页面动画
  • popExitAnim: 配置回退pop时原页面动画

通过上面的配置你可以看到如下效果👇


1615460611-5db6cfa49880e_articlex.gif

deepLink


我们回想一下对于多个Activity我需要实现deepLink效果,应该都是在AndroidManifest.xml中进行配置scheme、host等。而对于单个Activity也需要实现类似的效果,Navigation也提供了对应的实现,而且操作更简单。
Navigation提供的是deepLink标签,可以直接在nav_graph.xml进行配置,例如👇

  <fragment
        android:id="@+id/register_fragment"
        android:name="com.idisfkj.androidapianalysis.navigation.fragment.RegisterFragment"
        android:label="register_fragment"
        tools:layout="@layout/fragment_register">
 
        <action
            android:id="@+id/action_go_to_shop_list_page"
            app:destination="@id/shop_list_fragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_in_left"
            app:popEnterAnim="@anim/slide_out_left"
            app:popExitAnim="@anim/slide_out_right" />
 
        <deepLink app:uri="api://register/" />
 
    </fragment>

上面通过deepLink我配置了一个跳转到注册页RegisterFragment,写法非常简单,直接配置uri即可;同时还可以通过占位符配置传递参数,例如👇

<deepLink app:uri="api://register/{id}" />

这时我们就可以在注册页面通过argument获取key为id的数据。
当然要实现上面的效果,我们还需要一个前提,需要在AndroidManifest.xml中将我们的deepLink进行配置,在Activity中使用nav-graph标签👇

 <application
        ...
        android:theme="@style/AppTheme">
        <activity android:name=".navigation.NavigationMainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <nav-graph android:value="@navigation/nav_graph"/>
        </activity>
        ...
    </application>

现在只需将文章中的demo安装到手机上,再点击下面的link

jump to register api

之后就会启动App,并定位到注册界面。是不是非常简单呢?

最后我们再来看下效果👇

4001425053-5db6cfae97101_articlex.gif

有关Navigation暂时就到这里,通过这篇文章,希望你能够熟悉运用Navigation,并且发现单Activity的魅力。
如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!
以上文章本人搬运来的,我不生产文章,我只是文章的搬运工
原地址:https://segmentfault.com/a/1190000020839747

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