在JetPack中有一個組件是Navigation,顧名思義它是一個頁面導航組件,相對於其餘的第三方導航,不一樣的是它是專門爲Fragment的頁面管理所設計的。它對於單個Activity的App來講很是有用,由於以一個Activity爲架構的App頁面的呈現都是經過不一樣的Fragment來展現的。因此對於Fragment的管理相當重要。一般的實現都要本身維護Fragment之間的棧關係,同時要對Fragment的Transaction操做很是熟悉。爲了下降使用與維護成本,因此就有了今天的主角Navigation。java
若是你對JetPack的其它組件感興趣,推薦你閱讀我以前的系列文章,本篇文章目前爲JetPack系列的最後一篇。node
Android Architecture Components Part1:Room
Android Architecture Components Part2:LiveData
Android Architecture Components Part3:Lifecycle
Android Architecture Components Part4:ViewModel
Paging在RecyclerView中的應用,有這一篇就夠了
WorkManager從入門到實踐,有這一篇就夠了android
對於Navigation的使用,我將其概括於如下四點:git
在使用以前須要引入Navigation的依賴,而後咱們須要爲Navigation建立一個配置文件,它將位於res/navigation/nav_graph.xml。爲了方便理解文章中的代碼,我寫了一個Demo,你們能夠經過Android精華錄查看。github
在個人Demo中打開nav_graph.xml你將清晰的看到它們頁面間的關係紐帶api
一共有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與actionapp
以上是nav_graph.xml的基本配置。ide
在配置完以後,咱們還須要將其關聯到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>
除此以外,fragment的name屬性必須爲NavHostFragment,由於它會做爲咱們配置的全部fragment的管理者。具體經過內部的NavController中的NavigationProvider來獲取Navigator抽象實例,具體實現類是FragmentNavigator,因此最終經過它的navigate方法進行建立咱們配置的Fragment,而且添加到NavHostFragment的FrameLayout根佈局中。
此時若是咱們直接運行程序後發現已經能夠看到入口頁面WelcomeFragment
但點擊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) } }
本質是將action id與argument封裝成一個NavDirections,內部經過解析它來獲取action id與argument,從而執行跳轉。
而對於接受方ShopDetailFragment,插件頁面自動幫咱們生成一個ShopDetailFragmentArgs,以[類名]+Args的類。因此咱們須要作的也很是簡單👇
class ShopDetailFragment : Fragment() { private val args by navArgs<ShopDetailFragmentArgs>() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_shop_detail, container, false).apply { title.text = args.title add_cart.setOnClickListener(Navigation.createNavigateOnClickListener(ShopDetailFragmentDirections.actionGoToCartPage())) } } }
經過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>
對應四個動畫配置參數
經過上面的配置你能夠看到以下效果👇
咱們回想一下對於多個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
以後就會啓動App,並定位到註冊界面。是否是很是簡單呢?
最後咱們再來看下效果👇
有關Navigation暫時就到這裏,經過這篇文章,但願你可以熟悉運用Navigation,而且發現單Activity的魅力。
若是這篇文章對你有所幫助,你能夠順手點贊、關注一波,這是對我最大的鼓勵!
該庫的目的是結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者可以更快的掌握與理解所闡述的要點