Jetpack之Navigation全面剖析

Navigaion 是Android JetPack框架中的一員寸宵,是一套新的Fragment管理框架,可以幫助開發(fā)者很好的處理fragment之間的跳轉(zhuǎn)甲棍,優(yōu)雅的支持fragment之間的轉(zhuǎn)場動畫感猛,支持通過deeplink直接定位到fragment. 通過第三方的插件支持fragment之間安全的參數(shù)傳遞陪白,可以可視化的編輯各個組件之間的跳轉(zhuǎn)關(guān)系咱士。導航組件的推出序厉,使得我們在搭架應用架構(gòu)的時候脂矫,可以考慮一個功能模塊就是一個Activity, 模塊中每個子頁面使用Fragment實現(xiàn)庭再,使用Navigation處理Fragment之間的導航拄轻。更有甚者恨搓,設計一個單Activity的應用也不是沒有可能斧抱。最后還要提一點辉浦,Navigation不只是能管理Fragment宪郊,它還支持Activity弛槐,小伙伴們請注意這一點乎串。

下面我們來詳細介紹下Navigation的使用灌闺,在使用之前我們來先了解3個核心概念:
1桂对、Navigation Graph 這是Navigation的配置文件蕉斜,位于res/navigation/目錄下的xml文件. 這個文件是對導航中各個組件的跳轉(zhuǎn)關(guān)系的預覽宅此。在design模式下父腕,可以很清晰的看到組件之間關(guān)系璧亮,如圖1所示帘饶。
2及刻、NavHost 一個空白的父容器缴饭,承擔展示目的fragment的作用茴扁。源碼中父容器的實現(xiàn)是NavHostFragment峭火,在Activity中引入這個fragment才能使用Navigation的能力卖丸。
3稍浆、NavController 導航組件的跳轉(zhuǎn)控制器衅枫,管理導航的對象弦撩,控制NavHost中目標頁面的展示。

下面我們從一個簡單的例子先看下Navigation的基本用法感凤。

一 工程搭建

我們設計一個應用陪竿,分別實現(xiàn)首頁族跛,詳情頁庸蔼,購買頁姐仅,登錄頁掏膏,注冊頁佳簸。跳轉(zhuǎn)關(guān)系如下:
首頁->詳情頁->購買頁->首頁生均,首頁->登錄頁->注冊頁->首頁马胧。如果使用FragmentManager管理佩脊,需要對頁面創(chuàng)建,參數(shù)傳遞以及頁面回退做許多工作歇盼,下面我們看一下Navigation是如何管理這些頁面的旺遮。
首先耿眉,創(chuàng)建一個空白的工程.只包含一個activity. 修改工程的build.gradle文件使之包含下面的引用


def nav_version ="2.3.0"

// Java language implementation

implementation"androidx.navigation:navigation-fragment:$nav_version"

implementation"androidx.navigation:navigation-ui:$nav_version"

// Kotlin

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

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

// Dynamic Feature Module Support

implementation"androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

// Testing Navigation

androidTestImplementation"androidx.navigation:navigation-testing:$nav_version"

在“Project”窗口中,右鍵點擊 res 目錄筐骇,然后依次選擇 New > Android Resource File铛纬,此時系統(tǒng)會顯示 New Resource File 對話框棺弊。在 File name 字段中輸入名稱模她,例如“nav_graph”侈净。從 Resource type 下拉列表中選擇 Navigation畜侦,然后點擊 OK,生成的導航的xml (圖1中1位置)。

圖1

在可視化編輯模式下盯孙,點擊左上角的 icon(圖1中2位置)在xml中添加導航頁面. 添加完導航頁面,選中一個頁面骑晶,在右側(cè)的屬性欄桶蛔,可以為頁面添加跳轉(zhuǎn)action, deeplink和跳轉(zhuǎn)傳參仔雷。直接把兩個頁面之間連線电抚,也可以建立跳轉(zhuǎn)的action. 選中一條頁面間的連線蝙叛,可以編輯這個action借帘,為action添加轉(zhuǎn)場動畫,出棧屬性和傳參默認值狰挡。
右鍵點擊一個頁面加叁,在右鍵菜單中選擇edit, 就可以編輯對應fragment的xml文件.
都配置完成后,最終的導航圖就如圖2所示豫柬。
建立完導航圖烧给,我們還需要設置一個當做首頁的Fragment一啟動就展示,在要設置的Fragment上點擊右鍵榴鼎,選擇Set Start Destination巫财,將它設置為首頁桥言,設置完成后号阿,被選中的Fragment會有一個start標簽(圖1中3位置)當Activity啟動的時候扔涧,它會做為默認的頁面替換布局中的NavHostFragment。

下面是nav_graph.xml配置文件部分內(nèi)容湖雹,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/nav_graph"
    app:startDestination="@id/homeFragment">
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.navicasetest.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home" >
        <action
            android:id="@+id/action_homeFragment_to_detailFragment"
            app:destination="@id/detailFragment"
            app:enterAnim="@anim/slide_in_right"
            app:exitAnim="@anim/slide_out_left"
            app:popEnterAnim="@anim/slide_in_left"
            app:popExitAnim="@anim/slide_out_right" />
        <action
            android:id="@+id/action_homeFragment_to_loginFragment"
            app:destination="@id/loginFragment" />
    </fragment>
    <!--這里省略其他的fragment的配置-->
    ...
</navigation>

通過上面的配置,我們就完整的創(chuàng)建了一個導航圖。如下圖所示


圖2

下面就需要把導航添加到activity中诗箍。
在MainActivity的xml中,添加Navigation的容器 NavHostFragment氨距, NavHostFragment是系統(tǒng)類,我們后面分析它內(nèi)部的實現(xiàn)茬暇。xml配置如下

<fragment
    android:id="@+id/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"
    />

我們發(fā)現(xiàn)xml中有2個新的配置項,app:navGraph指定導航配置文件。app:defaultNavHost 置為true劈彪,標識是讓當前的導航容器NavHostFragment處理系統(tǒng)返回鍵沧奴,在 Navigation 容器中如果有頁面的跳轉(zhuǎn)纲菌,點擊返回按鈕會先處理 容器中 Fragment 頁面間的返回翰舌,處理完容器中的頁面,再處理 Activity 頁面的返回夜涕。如果值為 false 則直接處理 Activity 頁面的返回。

二 頁面跳轉(zhuǎn)和參數(shù)傳遞

頁面間的跳轉(zhuǎn)是通過action來實現(xiàn)驾胆,我們在HomeFragment中增加detail button的點擊響應丧诺,實現(xiàn)從首頁到詳情頁的跳轉(zhuǎn)驳阎,代碼實現(xiàn)如下呵晚。這里用到了NavController,我們后面會詳細介紹它金矛,這里先看它的用法娶耍。

mBtnGoDetail.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        NavController contorller = Navigation.findNavController(view);
        contorller.navigate(R.id.action_homeFragment_to_detailFragment);
    }
});

下面介紹如何在導航之間傳遞參數(shù)

1、Bundle方式

第一種方式是通過Bundle的方式奈应。NavController 的navigate方法提供了傳入?yún)?shù)是Bundle的方法,下面看一下實例代碼惩妇。從首頁傳參到商品詳情頁,首頁傳入?yún)?shù)

Bundle bundle = new Bundle();
bundle.putString("product_name","蘋果");
bundle.putFloat("price",10.5f);
NavController contorller = Navigation.findNavController(view);
contorller.navigate(R.id.action_homeFragment_to_detailFragment, bundle);

解析傳參

if (getArguments() != null) {
    mProductName = getArguments().getString("product_name");
    mPrice = getArguments().getFloat("price");
}

如果兩個fragment直接傳遞的參數(shù)較多氓皱,這種傳參方法就顯得很不友好,需要定義好多名字廷区,并且不能保證傳參的一致性,還容易出錯或者自定義一個model大脉,實現(xiàn)序列化方法琐驴。這樣也是比較繁瑣。

Android 系統(tǒng)還提供一種SafeArg的傳參方式宙刘。比較優(yōu)雅的處理參數(shù)的傳遞苍姜。

2、安全參數(shù)(SafeArg)

第一步悬包,在工程的build.gradle中添加下面的引用

classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0"

在app的build.gradle中增加

apply plugin: 'androidx.navigation.safeargs'

第二步衙猪,編輯navigation的xml文件 在本例中是nav_graph.xml. 可以通過可視化編輯,也可以直接編輯xml. 編輯完畢如下圖

<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.myapplication.DetailFragment"
    android:label="fragment_detail"
    tools:layout="@layout/fragment_detail" >
    <action
        android:id="@+id/action_detailFragment_to_payFragment"
        app:destination="@id/payFragment" />
    <argument
        android:name="productName"
        app:argType="string"
        android:defaultValue="unknow" />
    <argument
        android:name="price"
        app:argType="float"
        android:defaultValue="0" />
</fragment>

修改完xml后布近,編譯一下工程,在generate文件夾下會生成幾個文件撑瞧。如下圖


WechatIMG26.png

在首頁的跳轉(zhuǎn)函數(shù)中棵譬,寫下如下代碼

mBtnGoDetailBySafe.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Bundle bundle = new DetailFragmentArgs.Builder().setProductName("蘋果").setPrice(10.5f).build().toBundle();
        NavController contorller = Navigation.findNavController(view);
        contorller.navigate(R.id.action_homeFragment_to_detailFragment, bundle);
    }
});

在詳情頁接收傳參的地方,解析傳參的代碼

Bundle bundle = getArguments();
if(bundle != null){
    mProductName = DetailFragmentArgs.fromBundle(bundle).getProductName();
    mPrice = DetailFragmentArgs.fromBundle(bundle).getPrice();
}

DetailFragmentArgs內(nèi)部是使用了builder模式構(gòu)建傳參的bundle. 并且以getter,setter的方式設置屬性值预伺,這樣開發(fā)人員使用起來比較簡潔订咸,和使用普通java bean的方式基本一致。
細心的同學發(fā)現(xiàn)了酬诀,上面除了DetailFragmentArgs 還生成了2個direction類脏嚷,我們以HomeFragmentDirections為例看下用法,HomeFragmentDirections能夠直接提供跳轉(zhuǎn)的OnClickListener,

mBtnGoDetailBySafe.setOnClickListener(Navigation.createNavigateOnClickListener(HomeFragmentDirections.
        actionHomeFragmentToDetailFragment().setProductName("蘋果").setPrice(10.5f)));

分析HomeFragmentDirections代碼不難發(fā)現(xiàn)瞒御,本質(zhì)是將action id與argument封裝成一個NavDirections父叙,內(nèi)部通過解析它來獲取action id與argument,最終還是會執(zhí)行NavController的navigation方法執(zhí)行跳轉(zhuǎn)葵腹。下面看一下HomeFragmentDirections的內(nèi)部實現(xiàn)高每。

@NonNull
public static ActionHomeFragmentToDetailFragment actionHomeFragmentToDetailFragment(){
    return new ActionHomeFragmentToDetailFragment();
}

public static class ActionHomeFragmentToDetailFragment implements NavDirections {
    private final HashMap arguments = new HashMap();

    private ActionHomeFragmentToDetailFragment() {
    }

    @NonNull
    public ActionHomeFragmentToDetailFragment setProductName(@NonNull String productName) {
        if (productName == null) {
            throw new IllegalArgumentException("Argument \"productName\" is marked as non-null but was passed a null value.");
        }
        this.arguments.put("productName", productName);
        return this;
    }

    @NonNull
    public ActionHomeFragmentToDetailFragment setPrice(float price) {
        this.arguments.put("price", price);
        return this;
    }

    @Override
    public int getActionId() {
        return R.id.action_homeFragment_to_detailFragment;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    public String getProductName() {
        return (String) arguments.get("productName");
    }

    @SuppressWarnings("unchecked")
    public float getPrice() {
        return (float) arguments.get("price");
    }

}
3、ViewModel.

導航架構(gòu)中践宴,也可以通過ViewModel的方式共享數(shù)據(jù),后面我們還會講到使用ViewMode的必要性阻肩。每個Destination共享一份ViewModel雄右,這樣有利于及時監(jiān)聽數(shù)據(jù)變化空骚,同時把數(shù)據(jù)展示和存儲隔離纺讲。在上面的例子中,每個頁面都需要登錄狀態(tài)囤屹,我們把用戶登錄狀態(tài)封裝成UserViewModel熬甚,在需要監(jiān)聽登錄數(shù)據(jù)變化的頁面實現(xiàn)如下代碼

userViewModel.getUserModel().observe(getViewLifecycleOwner(), new Observer<UserModel>() {
    @Override
    public void onChanged(UserModel userModel) {
        if(userModel != null){
            //登錄成功,展示用戶名
            mUserName.setText(userModel.getUserName());
        } else {
            mUserName.setText("未登錄");
        }
    }
});

這樣當用戶登錄后肋坚,各個頁面都會得到通知乡括,刷新當前的昵稱展示。

三 動畫

多數(shù)場景下智厌,2個頁面之間的切換,我們希望有轉(zhuǎn)場動畫档礁,Navigation對動畫的支持也很簡單吝沫。可以在xml中直接配置配置惨险。

<fragment
    android:id="@+id/homeFragment"
    android:name="com.example.navicasetest.HomeFragment"
    android:label="fragment_home"
    tools:layout="@layout/fragment_home" >
    <action
        android:id="@+id/action_homeFragment_to_detailFragment"
        app:destination="@id/detailFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />
</fragment>

enterAnim: 配置進場時目標頁面動畫
exitAnim: 配置進場時原頁面動畫
popEnterAnim: 配置回退時目標頁面動畫
popExitAnim: 配置回退時原頁面動畫
配置完后,動畫展示如下


動畫

四 導航堆棧管理

Navigation 有自己的任務棧辫愉,每次調(diào)用navigate()函數(shù)栅受,都是一個入棧操作,出棧操作有以下幾種方式恭朗,下面詳細介紹幾種出棧方式和使用場景屏镊。

1、系統(tǒng)返回鍵

首先需要在xml中配置app:defaultNavHost="true"痰腮,才能讓導航容器攔截系統(tǒng)返回鍵而芥,點擊系統(tǒng)返回鍵,是默認的出棧操作膀值,回退到上一個導航頁面棍丐。如果當棧中只剩一個頁面的時候,系統(tǒng)返回鍵將由當前Activity處理沧踏。

2歌逢、自定義返回鍵

如果頁面上有返回按鈕,那么我們可以調(diào)用popBackStack()或者navigateUp()返回到上一個頁面翘狱。我們先看一下navigateUp源碼

public boolean navigateUp() {
    if (getDestinationCountOnBackStack() == 1) {
        // If there's only one entry, then we've deep linked into a specific destination
        // on another task so we need to find the parent and start our task from there
        NavDestination currentDestination = getCurrentDestination();
        int destId = currentDestination.getId();
        NavGraph parent = currentDestination.getParent();
        while (parent != null) {
            if (parent.getStartDestination() != destId) {
                //省略部分代碼
                return true;
            }
            destId = parent.getId();
            parent = parent.getParent();
        }
        // We're already at the startDestination of the graph so there's no 'Up' to go to
        return false;
    } else {
        return popBackStack();
    }
}

從源碼可以看出秘案,當棧中任務大于1個的時候,兩個函數(shù)沒什么區(qū)別。當棧中只有一個導航首頁(start destination)的時候踏烙,navigateUp()不會彈出導航首頁师骗,它什么都不做,直接返回false.
popBackStack則會把導航首頁也出棧讨惩,但是由于沒有回退到任何其他頁面,此時popBackStack會返回false, 如果此時又繼續(xù)調(diào)用navigate()函數(shù)寒屯,會發(fā)生exception荐捻。所以google官網(wǎng)說不建議把導航首頁也出棧。如果導航首頁出棧了处面,此時需要關(guān)閉當前Activity≈浅瘢或者跳轉(zhuǎn)到其他導航頁面斯稳。示例代碼如下挣惰。

...

if (!navController.popBackStack()) {
    // Call finish() on your Activity
    finish();
}
3憎茂、popUpTo 和 popUpToInclusive

還有一種出棧方式,就是通過設置popUpTo和popUpToInclusive在導航過程中彈出頁面赏枚。
popUpTo指出棧直到某目標饿幅,字面意思比較難理解栗恩,我們看下面這個例子磕秤。
假設有A,B,C 3個頁面汉操,跳轉(zhuǎn)順序是 A to B磷瘤,B to C采缚,C to A扳抽。
依次執(zhí)行幾次跳轉(zhuǎn)后,棧中的順序是A>B>C>A>B>C>A贮尉。此時如果用戶按返回鍵猜谚,會發(fā)現(xiàn)反復出現(xiàn)重復的頁面魏铅,此時用戶的預期應該是在A頁面點擊返回,應該退出應用鸿竖。
此時就需要在C到A的action中設置popUpTo="@id/a". 這樣在C跳轉(zhuǎn)A的過程中會把B,C出棧缚忧。但是還會保留上一個A的實例糕非,加上新創(chuàng)建的這個A的實例朽肥,就會出現(xiàn)2個A的實例. 此時就需要設置
popUpToInclusive=true. 這個配置會把上一個頁面的實例也彈出棧篱昔,只保留新建的實例旱爆。
下面再分析一下設置成false的場景。還是上面3個頁面山林,跳轉(zhuǎn)順序A to B驼抹,B to C. 此時在B跳C的action中設置 popUpTo=“@id/a”, popUpToInclusive=false. 跳到C后,此時棧中的順序是AC明也。B被出棧了温数。如果設置popUpToInclusive=true. 此時棧中的保留的就是C撑刺。AB都被出棧了够傍。
在咱們的示例中,在注冊界面愕撰,用戶注冊完成后带迟,希望直接返回首頁仓犬。這樣我們就需要在從RegisterFragment到HomeFragment的跳轉(zhuǎn)過程中搀继,彈出之前棧中的首頁叽躯,登錄頁和注冊頁,添加如下配置既可達到我們想要的效果黑滴。

<fragment
    android:id="@+id/registerFragment"
    android:name="com.example.navicasetest.RegisterFragment"
    android:label="fragment_register"
    tools:layout="@layout/fragment_reg" >
    <action
        android:id="@+id/action_registerFragment_to_homeFragment"
        app:destination="@id/homeFragment"
        app:popUpTo="@id/homeFragment"
        app:popUpToInclusive="true"/>
</fragment>

五 DeepLink

Navigation組件提供了對深層鏈接(DeepLink)的支持袁辈。通過該特性晚缩,我們可以利用PendingIntent或者一個真實的URL鏈接,直接跳轉(zhuǎn)到應用程序的某個destination
下面我們分別看一下這兩種的使用方式卿泽。

1签夭、PendingIntent

創(chuàng)建一個通知欄第租,通過Navigition 創(chuàng)建PendingIntent.


private void createNotification(){

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        int importance = NotificationManager.IMPORTANCE_DEFAULT;
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "ChannelName", importance);
        channel.setDescription("description");
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }

    NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle("促銷水果")
            .setContentText("香蕉")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(getPendingIntent())//設置PendingIntent
            .setAutoCancel(true);

    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    notificationManager.notify(100001, builder.build());
}

private PendingIntent getPendingIntent() {
    Bundle bundle = new Bundle();
    bundle.putString("productName", "香蕉");
    bundle.putFloat("price",6.66f);
    return Navigation
            .findNavController(this,R.id.fragment)
            .createDeepLink()
            .setGraph(R.navigation.nav_graph)
            .setDestination(R.id.detailFragment)
            .setArguments(bundle)
            .createPendingIntent();
}

在DetailFragment, 解析傳參即可。參考上面的傳參小節(jié)券犁。效果如下所示


notification
2粘衬、URL連接

URL的使用也比較簡單稚新,我們下面給商品詳情頁(DetailFragment)添加deeplink支持,URL格式如下笤妙。
www.mywebsite.com/detail?productName={productName}price={price}
首先,需要在導航xml中喊熟,添加deeplink支持铃诬,添加完成xml如下

<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.navicasetest.DetailFragment"
    android:label="fragment_detail"
    tools:layout="@layout/fragment_detail">
    <action
        android:id="@+id/action_detailFragment_to_payFragment"
        app:destination="@id/payFragment"
        app:enterAnim="@anim/slide_in_right"
        app:exitAnim="@anim/slide_out_left"
        app:popEnterAnim="@anim/slide_in_left"
        app:popExitAnim="@anim/slide_out_right" />
    <argument
        android:name="productName"
        android:defaultValue="unknow"
        app:argType="string" />
    <argument
        android:name="price"
        android:defaultValue="0.0f"
        app:argType="float" />
    <deepLink
        android:autoVerify="true"
        app:uri="www.mywebsite.com/detail?productName={productName}price={price}" />
</fragment>

然后兵志,在Manifest文件中想罕,添加如下配置

<nav-graph android:value="@navigation/nav_graph"/>

我們的DetailFragment中已經(jīng)做了對參數(shù)productName和price的解析按价。
安裝app后,使用adb 命令測試deeplink連接

adb shell am start -a android.intent.action.VIEW -d "http://www.mywebsite.com/detail?productName="香蕉"price=10"

執(zhí)行adb命令后框产,商品詳情頁被正常拉起。

五 場景對比

上面介紹了Navigation的基本用法戒突,這一小節(jié)我們將構(gòu)建一個頁面,分別看一下使用Navigation和不使用Navigation對頁面架構(gòu)的影響膝舅。
在我們以往的項目開發(fā)過程中, 業(yè)務復雜且包含的模塊比較多的頁面, 我們經(jīng)常用獨立的fragment來承擔不同的業(yè)務子頁面仍稀,但是fragment之間的跳轉(zhuǎn)技潘,轉(zhuǎn)場動畫享幽,以及回退棧管理值桩,開發(fā)者需要自己實現(xiàn)相關(guān)邏輯奔坟。我們看下面的例子:


實現(xiàn)上面包含3個tab的首頁,常規(guī)做法是使用BottomNavigationView + fragment來搭架澜建。代碼如下, 需要自己管理fragment的創(chuàng)建以及加載蝌以。

public class MainActivity2 extends AppCompatActivity {

    private int laseSelectPos = 0;
    private Fragment[] fragments;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        HomeFragment homeFragment = new HomeFragment();
        DashboardFragment dashboardFragment = new DashboardFragment();
        NotificationsFragment notificationsFragment = new NotificationsFragment();
        fragments = new Fragment[]{homeFragment, dashboardFragment, notificationsFragment};

        laseSelectPos = 0;
        
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.fl_con, homeFragment)
                .show(homeFragment)//展示
                .commit();
        BottomNavigationView navView = findViewById(R.id.nav_vew_2);

        navView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() {
            @Override
            public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                switch (item.getItemId()){
                    case R.id.navigation_home:
                        if (0 != laseSelectPos) {
                            setDefaultFragment(0);
                            laseSelectPos = 0;
                        }
                        return true;
                    case R.id.navigation_dashboard:
                        if (1 != laseSelectPos) {
                            setDefaultFragment(1);
                            laseSelectPos = 1;
                        }
                        return true;
                    case R.id.navigation_notifications:
                        if (2 != laseSelectPos) {
                            setDefaultFragment(2);
                            laseSelectPos = 2;
                        }
                        return true;
                }
                return false;
            }
        });
    }

    private void setDefaultFragment( int index) {
        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
        transaction.replace(R.id.fl_con, fragments[index]);
        transaction.commit();
    }
}

配置文件如下:

<?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"
    android:paddingTop="?attr/actionBarSize">


    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_vew_2"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <FrameLayout
        android:id="@+id/fl_con"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toTopOf="@+id/nav_vew_2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

如果我們使用Navigation + BottomNavigationView來搭建上述要頁面
代碼如下:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BottomNavigationView navView = findViewById(R.id.nav_view);
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
                .build();
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(navView, navController);
    }

}

配置文件如下

<?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"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <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:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

比較上面2份代碼悼潭,明顯Navigation的方式實現(xiàn)更簡潔舰褪,框架幫我們做了好多創(chuàng)建和管理的工作占拍,我們只要專注每個fragment的業(yè)務即可晃酒。例子中只是單純的展示fragment, 后面如果要加deeplink跳轉(zhuǎn),轉(zhuǎn)場動畫等需求彰导,就會更加體現(xiàn)navigation優(yōu)勢位谋。

六 源碼分析

Navigation暴露給開發(fā)者的就是NavHostFragment盖腿,NavController以及導航圖。導航圖又再xml文件中設置給了NavHostFragment鸟款。所以我們就主要分析這兩個類NavHostFragment和NavController。我們帶著下面幾個問題來分析下源碼:

  1. 導航圖是如何解析组哩?
  2. 頁面跳轉(zhuǎn)是如何實現(xiàn)的伶贰?
  3. 為什么從一個靜態(tài)方法隨便傳入一個view,就能拿到NavController實例?
  4. 導航框架不僅支持fragment還支持activity, 是如何做到的荠诬?

為了避免大量的代碼影響閱讀體驗,后面的源碼分析只把關(guān)鍵的代碼做了展示方椎,本文中未列出的代碼棠众,讀者可以自行參考源碼空盼。

1我注、NavHostFragment

要在某個Activity中實現(xiàn)導航但骨,首先就是要在xml中引入NavHostFragment,xml中通過指定app:navGraph="@navigation/nav_graph"來指定導航圖, 那么應該是這個Fragment來負責解析并加載導航圖吼野。我們就從這個Fragment創(chuàng)建流程入手闷哆,來看一下源碼抱怔。
1屈留、onInflate 在這個流程中解析出我們上面提到的在xml配置的兩個參數(shù)defaultNavHost,
和navGraph勇蝙,并保存在成員變量中 mGraphId浅蚪,mDefaultNavHost惜傲。

final TypedArray navHost = context.obtainStyledAttributes(attrs,
                androidx.navigation.R.styleable.NavHost);
final int graphId = navHost.getResourceId(
        androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
    mGraphId = graphId;
}
navHost.recycle();

final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
    mDefaultNavHost = true;
}
a.recycle();

2时甚、onCreate, 在OnCreate中荒适,我們發(fā)現(xiàn)了NavController是在這里創(chuàng)建的, 這就說明一個導航圖對應一個NavController刀诬,在OnCreate中還把上面的mGraphId,設置給了NavController.

mNavController = new NavHostController(context);
//省略部分代碼
if (mGraphId != 0) {
    // Set from onInflate()
    mNavController.setGraph(mGraphId);
} else {
    // See if it was set by NavHostFragment.create()
    final Bundle args = getArguments();
    final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
    final Bundle startDestinationArgs = args != null
            ? args.getBundle(KEY_START_DESTINATION_ARGS)
            : null;
    if (graphId != 0) {
        mNavController.setGraph(graphId, startDestinationArgs);
    }
}

3糠馆、onCreateView 在這個函數(shù)中又碌,只是創(chuàng)建了一個FragmentContainerView. 這個View是一個FrameLayout, 用于加載導航的Fragment

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                         @Nullable Bundle savedInstanceState) {
    FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
    // When added via XML, this has no effect (since this FragmentContainerView is given the ID
    // automatically), but this ensures that the View exists as part of this Fragment's View
    // hierarchy in cases where the NavHostFragment is added programmatically as is required
    // for child fragment transactions
    containerView.setId(getContainerId());
    return containerView;
}

4癌别、onViewCreated 在這個函數(shù)中皂岔,把NavController設置給了父布局的view的中的ViewTag中。這里的設計比較關(guān)鍵规个,為什么要放到tag中呢?其實這樣的設計是為了讓我們外部獲取這個實例比較便捷姓建,我們上面的問題3的答案就在這里诞仓,我們先看一下查找NavController的函數(shù)Navigation.findNavController(View),請注意API的設計速兔,似乎傳遞任意一個 view的引用都可以獲取 NavController墅拭,這里就是通過遞歸遍歷view的父布局,查找是否有view含有id為R.id.nav_controller_view_tag的tag, tag有值就找到了NavController谍婉。如果tag沒有值.說明當前父容器沒有NavController.這里我們貼一下保存和查找的代碼。

public static void setViewNavController(@NonNull View view,
                                        @Nullable NavController controller) {
    view.setTag(R.id.nav_controller_view_tag, controller);
}

@Nullable
private static NavController findViewNavController(@NonNull View view) {
    while (view != null) {
        NavController controller = getViewNavController(view);
        if (controller != null) {
            return controller;
        }
        ViewParent parent = view.getParent();
        view = parent instanceof View ? (View) parent : null;
    }
    return null;
}
   

以上4步箱季,就是NavHostFragment的主要工作掘殴,我們通過上面的分析可以看到服爷,這個Fragment沒有承擔任何Destination的創(chuàng)建和導航工作笼踩。也沒有看到導航圖的解析工作于购,這個Fragment只是創(chuàng)建了個容器嫌吠,創(chuàng)建了NavController泥栖,然后把只是單純的把mGraphId設置給了NavController钢颂。我們猜測導航的解析和創(chuàng)建工作應該都在NavController中。我們來看一下NavController的源碼。

2救鲤、NavController

導航的主要工作都在NavController中稀颁,涉及xml解析垢啼,導航堆棧管理,導航跳轉(zhuǎn)等方面。下面我們帶著上面剩余的3個問題,分析下NavController的實現(xiàn)怀读。

  1. 上面我們提到NavHostFragment把導航文件的資源id傳給了NavController啤誊,我們繼續(xù)分析代碼發(fā)現(xiàn),NavController把導航xml文件傳遞給了NavInflater, NavInflater主要負責解析導航xml文件,解析完畢后摊沉,生成NavGraph,NavGraph是個目標管理容器余耽,保存著xml中配置的導航目標NavDestination扛邑。
@NonNull
private NavDestination inflate(@NonNull Resources res, @NonNull XmlResourceParser parser,
                               @NonNull AttributeSet attrs, int graphResId)
        throws XmlPullParserException, IOException {
    Navigator<?> navigator = mNavigatorProvider.getNavigator(parser.getName());
    final NavDestination dest = navigator.createDestination();

    dest.onInflate(mContext, attrs);

    final int innerDepth = parser.getDepth() + 1;
    int type;
    int depth;
    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
            && ((depth = parser.getDepth()) >= innerDepth
            || type != XmlPullParser.END_TAG)) {
        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        if (depth > innerDepth) {
            continue;
        }

        final String name = parser.getName();
        if (TAG_ARGUMENT.equals(name)) {
            inflateArgumentForDestination(res, dest, attrs, graphResId);
        } else if (TAG_DEEP_LINK.equals(name)) {
            inflateDeepLink(res, dest, attrs);
        } else if (TAG_ACTION.equals(name)) {
            inflateAction(res, dest, attrs, parser, graphResId);
        } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
            final TypedArray a = res.obtainAttributes(
                    attrs, androidx.navigation.R.styleable.NavInclude);
            final int id = a.getResourceId(
                    androidx.navigation.R.styleable.NavInclude_graph, 0);
            ((NavGraph) dest).addDestination(inflate(id));
            a.recycle();
        } else if (dest instanceof NavGraph) {
            ((NavGraph) dest).addDestination(inflate(res, parser, attrs, graphResId));
        }
    }

    return dest;
}
  1. 導航目標解析完畢薪伏,具體的頁面跳轉(zhuǎn)是如何實現(xiàn)的呢挡育,在使用過程中我們調(diào)用的是NavController的navigate函數(shù),抽絲剝繭疲恢,發(fā)現(xiàn)導航最終調(diào)用的是Navigator的navigate函數(shù)揍移。
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
        node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
NavDestination newDest = navigator.navigate(node, finalArgs,
        navOptions, navigatorExtras);

我們看到導航的具體實現(xiàn)是Navigator铃在,我們上面的例子是以Fragment為導航目標鹰溜,但是Navigation 的目標對象不只是Fragment, 還可以是Activity兔港,后面可能還會擴展其他種類, 這里谷歌把導航抽象成了Navigator,NavController中沒有持有具體的導航種類,而是持有的抽象類Navigator, 把所有Navigator的實例保存在了NavigatorProvider中. 這里就運用了設計模式中的依賴倒置原則侮繁,要面向接口編程,而不是具體實現(xiàn)。同時也符合了開閉原則,后面在擴展新的導航種類润歉,不會影響到現(xiàn)有的種類贩汉。通過以上的分析赐稽,問題2和問題4也就得到了解答。
我們以FragmentNavigator為例考蕾,看一下具體的導航邏輯的實現(xiàn)。只分析部分關(guān)鍵代碼片段

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();



ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);

從以上代碼可以看出挺尿,F(xiàn)ragment實例是通過instantiateFragment創(chuàng)建的,這個函數(shù)中是通過反射的方式創(chuàng)建的Fragment實例编矾,F(xiàn)ragment還是通過FragmentManager進行管理熟史,是用replace方法替換新的Fragment, 這就是說每次導航產(chǎn)生的Fragment都是一個新的實例,不會保存之前Fragment的狀態(tài)窄俏。這樣的話蹂匹,可能會造成數(shù)據(jù)不同步的現(xiàn)象。所以google建議導航和ViewModel配合使用效果更佳凹蜈。

綜上所述限寞,NavController是導航的核心類忍啸,它負責頁面加載,頁面導航履植,和堆棧管理计雌。但是這些邏輯沒有都耦合在這個類中,而是采用組合的方式玫霎,把這些實現(xiàn)都拆分成了單獨的模塊凿滤。NavController需要實現(xiàn)哪些功能,調(diào)用相應功能即可庶近。

七 總結(jié)

上面我們列舉了導航的基本用法以及源碼分析翁脆,通過上面的學習,大家也了解到了鼻种,導航組件是一個頁面的管理框架反番,創(chuàng)建簡潔,使用方便叉钥,在構(gòu)架業(yè)務復雜的頁面時罢缸,架構(gòu)清晰,功能多樣沼侣,可以使開發(fā)者可以專注于業(yè)務邏輯的開發(fā)祖能,是一個優(yōu)秀的框架歉秫。我們在學習的過程中蛾洛,不僅要學會如何使用,還要深入的學習其架構(gòu)原理雁芙,為我們以后的項目架構(gòu)轧膘,提供可借鑒的方案。

參考文獻:
https://developer.android.google.cn/guide/navigation/navigation-getting-started
http://www.reibang.com/p/ad040aab0e66

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末兔甘,一起剝皮案震驚了整個濱河市谎碍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洞焙,老刑警劉巖蟆淀,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異澡匪,居然都是意外死亡熔任,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門唁情,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疑苔,“玉大人,你說我怎么就攤上這事甸鸟〉敕眩” “怎么了兵迅?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長薪贫。 經(jīng)常有香客問我恍箭,道長,這世上最難降的妖魔是什么后雷? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任季惯,我火速辦了婚禮,結(jié)果婚禮上臀突,老公的妹妹穿的比我還像新娘勉抓。我一直安慰自己,他們只是感情好候学,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布藕筋。 她就那樣靜靜地躺著,像睡著了一般梳码。 火紅的嫁衣襯著肌膚如雪隐圾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天掰茶,我揣著相機與錄音暇藏,去河邊找鬼。 笑死濒蒋,一個胖子當著我的面吹牛盐碱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播沪伙,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼瓮顽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了围橡?” 一聲冷哼從身側(cè)響起暖混,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翁授,沒想到半個月后拣播,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡收擦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年贮配,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炬守。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡牧嫉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情酣藻,我是刑警寧澤曹洽,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站辽剧,受9級特大地震影響送淆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜怕轿,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一偷崩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撞羽,春花似錦阐斜、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至邻奠,卻和暖如春笤喳,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碌宴。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工杀狡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贰镣。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓呜象,卻偏偏與公主長得像,于是被迫代替她去往敵國和親八孝。 傳聞我的和親對象是個殘疾皇子董朝,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348