Android Navigation的四大要點你都知道嗎?

在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你將清晰的看到它們頁面間的關係紐帶

1.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

2.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);
        ...
    }

源碼就分析到這裏了,如果需要深入瞭解,建議閱讀NavHostFragmentNavControllerNavigatorProviderFragmentNavigator

傳參

以上是頁面的無參跳轉,那麼對於有參跳轉又該如何呢?

大家想到的應該都是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>

對應四個動畫配置參數

  • enterAnim: 配置進場時目標頁面動畫
  • exitAnim: 配置進場時原頁面動畫
  • popEnterAnim: 配置回退pop時目標頁面動畫
  • popExitAnim: 配置回退pop時原頁面動畫

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

3.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,並定位到註冊界面。是不是非常簡單呢?

最後我們再來看下效果👇

4.gif

有關Navigation暫時就到這裏,通過這篇文章,希望你能夠熟悉運用Navigation,並且發現單Activity的魅力。

如果這篇文章對你有所幫助,你可以順手點贊、關注一波,這是對我最大的鼓勵!

項目地址

Android精華錄

該庫的目的是結合詳細的Demo來全面解析Android相關的知識點, 幫助讀者能夠更快的掌握與理解所闡述的要點

Android精華錄

Android補給站.jpg

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章