在JetPack中有一個組件是Navigation布持,顧名思義它是一個頁面導航組件,相對于其他的第三方導航,不同的是它是專門為Fragment的頁面管理所設計的澳眷。它對于單個Activity的App來說非常有用,因為以一個Activity為架構的App頁面的呈現(xiàn)都是通過不同的Fragment來展示的蛉艾。所以對于Fragment的管理至關重要钳踊。通常的實現(xiàn)都要自己維護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的跳轉(zhuǎn)與數(shù)據(jù)傳遞
- Navigation的頁面動畫
- Navigation的deepLink
配置
在使用之前需要引入Navigation的依賴,然后我們需要為Navigation創(chuàng)建一個配置文件泰演,它將位于res/navigation/nav_graph.xml呻拌。為了方便理解文章中的代碼,我寫了一個Demo睦焕,大家可以通過Android精華錄查看藐握。
在我的Demo中打開nav_graph.xml你將清晰的看到它們頁面間的關系紐帶
一共有6個頁面,最左邊的為程序入口頁面复亏,它們間的線條指向為它們間可跳轉(zhuǎn)的方向趾娃。
我們再來看它們的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屬性來定義關聯(lián)的fragment
- action: 意圖笤成,可以理解為Intent,即跳轉(zhuǎn)的行為眷茁。通過destination來關聯(lián)將要跳轉(zhuǎn)的目標fragment炕泳。
以上是nav_graph.xml的基本配置。
在配置完之后上祈,我們還需要將其關聯(lián)到Activity中培遵。因為所有的Fragment都離不開Activity。
Navigation為我們提供了兩個配置參數(shù): 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的管理者揍很。具體通過內(nèi)部的NavController中的NavigationProvider來獲取Navigator抽象實例郎楼,具體實現(xiàn)類是FragmentNavigator万伤,所以最終通過它的navigate方法進行創(chuàng)建我們配置的Fragment,并且添加到NavHostFragment的FrameLayout根布局中呜袁。
此時如果我們直接運行程序后發(fā)現(xiàn)已經(jīng)可以看到入口頁面WelcomeFragment
但點擊register等操作你會發(fā)現(xiàn)點擊跳轉(zhuǎn)無效敌买,所以接下來我們需要為其添加跳轉(zhuǎn)
跳轉(zhuǎn)
由于我們之前已經(jīng)在nav_graph.xml中定義了action,所以跳轉(zhuǎn)的接入非常方便傅寡,每一個action的關聯(lián)跳轉(zhuǎn)只需一行代碼??
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放妈,內(nèi)部原理是先獲取到對應的NavController,通過點擊的view來遍歷找到最外層的parent view荐操,因為最外層的parent view會在配置文件導入時,即NavHostFragment中的onViewCreated方法中進行關聯(lián)對應的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);
}
}
}
然后再調(diào)用navigate進行頁面跳轉(zhuǎn)處理珍策,最終通過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
傳參
以上是頁面的無參跳轉(zhuǎn),那么對于有參跳轉(zhuǎn)又該如何呢蹭劈?
大家想到的應該都是bundle疗绣,將傳遞的數(shù)據(jù)填入到bundle中。沒錯Navigator提供的navigate方法可以進行傳遞bundle數(shù)據(jù)??
findNavController().navigate(R.id.action_go_to_shop_detail_page, bundleOf("title" to "I am title"))
這種傳統(tǒng)的方法在傳遞數(shù)據(jù)類型上并不能保證其一致性铺韧,為了減少人為精力上的錯誤多矮,Navigation提供了一個Gradle插件,專門用來保證數(shù)據(jù)的類型安全哈打。
使用它的話需要引入該插件塔逃,方式如下??
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"
而它的使用方式也很簡單,首先參數(shù)需要在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>
現(xiàn)在我們從ShopListFragment跳轉(zhuǎn)到ShopDetailFragment湾盗,需要在ShopListFragment的對應action中添加argument,聲明對應的參數(shù)類型與參數(shù)名立轧,也可以通過defaultValue定義參數(shù)的默認值與nullable標明是否可空格粪。對應的ShopDetailFragment接收參數(shù)也是一樣。
另外popUpTo與popUpToInclusive屬性是為了實現(xiàn)跳轉(zhuǎn)到CartFragment時達到SingleTop效果氛改。
下面我們直接看在代碼中如何使用這些配置的參數(shù)帐萎,首先是在ShopListFragment中??
holder.item.setOnClickListener(Navigation.createNavigateOnClickListener(ShopListFragmentDirections.actionGoToShopDetailPage(shopList[position])))
還是創(chuàng)建一個createNavigateOnClickListener,只不過現(xiàn)在傳遞的不再是跳轉(zhuǎn)的action id平窘,而是通過插件自動生成的ShopListFragmentDirections.actionGoToShopDetailPage方法吓肋。一旦我們?nèi)缟吓渲昧薬rgument,插件就會自動生成一個以[類名]+Directions的類瑰艘,而自動生成的類本質(zhì)是做了跳轉(zhuǎn)與參數(shù)的封裝是鬼,源碼如下??
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對象饺蔑,它其中包含了傳遞過來的頁面數(shù)據(jù)。
動畫
在action中不僅可以配置跳轉(zhuǎn)的destination揭厚,還可以定義對應頁面的轉(zhuǎn)場動畫示绊,使用非常簡單??
<?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>
對應四個動畫配置參數(shù)
- enterAnim: 配置進場時目標頁面動畫
- exitAnim: 配置進場時原頁面動畫
- popEnterAnim: 配置回退pop時目標頁面動畫
- popExitAnim: 配置回退pop時原頁面動畫
通過上面的配置你可以看到如下效果??
deepLink
我們回想一下對于多個Activity我需要實現(xiàn)deepLink效果,應該都是在AndroidManifest.xml中進行配置scheme囤耳、host等篙顺。而對于單個Activity也需要實現(xiàn)類似的效果,Navigation也提供了對應的實現(xiàn)充择,而且操作更簡單德玫。
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我配置了一個跳轉(zhuǎn)到注冊頁RegisterFragment宰僧,寫法非常簡單,直接配置uri即可观挎;同時還可以通過占位符配置傳遞參數(shù)琴儿,例如??
<deepLink app:uri="api://register/{id}" />
這時我們就可以在注冊頁面通過argument獲取key為id的數(shù)據(jù)。
當然要實現(xiàn)上面的效果嘁捷,我們還需要一個前提造成,需要在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>
現(xiàn)在只需將文章中的demo安裝到手機上雄嚣,再點擊下面的link
jump to register api
之后就會啟動App晒屎,并定位到注冊界面。是不是非常簡單呢现诀?
最后我們再來看下效果??
有關Navigation暫時就到這里夷磕,通過這篇文章,希望你能夠熟悉運用Navigation仔沿,并且發(fā)現(xiàn)單Activity的魅力坐桩。
如果這篇文章對你有所幫助,你可以順手點贊封锉、關注一波绵跷,這是對我最大的鼓勵!
以上文章本人搬運來的成福,我不生產(chǎn)文章碾局,我只是文章的搬運工
原地址:https://segmentfault.com/a/1190000020839747