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位置)。
在可視化編輯模式下盯孙,點擊左上角的 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)建了一個導航圖。如下圖所示
下面就需要把導航添加到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文件夾下會生成幾個文件撑瞧。如下圖
在首頁的跳轉(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é)券犁。效果如下所示
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。我們帶著下面幾個問題來分析下源碼:
- 導航圖是如何解析组哩?
- 頁面跳轉(zhuǎn)是如何實現(xiàn)的伶贰?
- 為什么從一個靜態(tài)方法隨便傳入一個view,就能拿到NavController實例?
- 導航框架不僅支持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)怀读。
- 上面我們提到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;
}
- 導航目標解析完畢薪伏,具體的頁面跳轉(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