Navigation學習總結

Google 在官方開發工具包中(Android Jetpack)中提供了一個用于Android app導航的全新框架“Navigation”,配合IDE可以很方便的查看App中頁面之間或模塊之間的關聯關系,這個跟IOS中StoryBoard很像。
官方文檔:The Navigation Architecture Component
官方教程:Navigation Codelab
官方Demo:android-navigation

概述



使用Navigation Architecture Component(后面簡稱Navigation)不但可以實現App間復雜的導航關系而且還使得導航關系可視化在這一點上要比一些第三方的導航框架(ARouter等)要好的多。在Navigation框架中引入了以下幾個概念需要說明下:
Destination -- 直譯過來就是目的地的意思,結合Android開發環境理解,指的就是頁面或者模塊等。Activity、Fragment、Graph等都可以充當一個Destination。

  • Action -- 就是頁面間的導航關系用于連接Destination
  • Graph -- 多個Destination通過Action連接起來就是一個Graph

Navigation 框架支持在Fragment、Activity、Graph、SubGraph、自定義Destination之間導航。包括前面提到的功能總結起來Navigation框架總共提供了以下一系列的附加功能,用于輔助開發簡化開發流程:

  • 處理Fragment的事務(Transactions)
  • 為返回操作(Back & Up)提供正確的默認實現
  • 為動畫和過渡提供標準的資源
  • 支持Deep link
  • 通過很少的額外操作就可以支持Navigation UI,例如Navigation Drawer、Bottom Navigation等
  • 使頁面間傳值變的更加安全
  • 通過IDE可以實現可視化編輯

在使用Navigation框架的時候有以下幾點需要注意:

  • 使用一個棧來代表App的導航狀態
  • 必須要有一個固定的起始Destination
  • 不能使用Up button退出你的程序
  • 在App任務中向上和返回按鈕是等價的
  • 深度鏈接到目標或導航到相同的目標應產生相同的堆棧

Navigation的使用


配置IDE

要是使用Navigation框架要求你的Android Studio版本必須是3.2+,如果你的Android Studio版本是3.2,你需要進入IDE的設置界面找到“Enable Navigation Editor”選項并選中(需要重新啟動Android Studio)。


image.png

配置項目



創建一個標準的Android Project然后在配置文件中(build.gradle)中配置Navigation依賴(這里需要注意Android官方更新了Support Library的命名空間具體參考官方文檔),具體配置方式如下:

dependencies {
    def nav_version = "1.0.0-alpha08"

    implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin
    implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin

    // optional - Test helpers
    // this library depends on the Kotlin standard library
    androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version"
}

如果要使用“Safe args”特性還需要增加如下配置:

apply plugin: "androidx.navigation.safeargs"

創建Navigation Graph



關于這里官方教程有點啰嗦總結起來就是以下幾步:

  1. 在你項目工程的“res”目錄下創建“navigation”文件夾
  2. 在新建的“navigation”目錄下右鍵新建一個“Navigation resource file”

完成后預覽界面和源碼界面分別如下圖所示:


image.png

image.png

根據提示點擊"+"創建幾個測試用的Destination,完成后如圖:

image.png

對應的源碼如下:

<?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/setting_nav_graph"
            app:startDestination="@id/mainSettingFragment">

    <fragment
            android:id="@+id/mainSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.MainSettingFragment"
            android:label="fragment_main_setting"
            tools:layout="@layout/fragment_main_setting">
        <action
                android:id="@+id/action_mainSettingFragment_to_cameraSettingFragment"
                app:destination="@id/cameraSettingFragment"/>
    </fragment>
    <fragment
            android:id="@+id/cameraSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.CameraSettingFragment"
            android:label="fragment_camera_setting"
            tools:layout="@layout/fragment_camera_setting"/>
</navigation>

然后編輯Activity的布局文件,在布局文件中增加“NavHostFragment”,其中"main_nav"就是剛剛新建的Navigation Graph文件名(main_nav.xml)代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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"
    tools:context=".MainActivity">

    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"

        app:defaultNavHost="true"
        app:navGraph="@navigation/main_nav" />

</android.support.constraint.ConstraintLayout>

頁面跳轉



要實現從MainSettingFragment到CameraSettingFragment我們只需要在MainSettingFragment中的按鈕點擊事件中添加如下代碼:

view.findViewById<Button>(R.id.camera)
.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_mainSettingFragment_to_cameraSettingFragment))

“action_mainSettingFragment_to_cameraSettingFragment”就是你定義的Action的id。

頁面傳值



官方提倡通過這種方式傳遞一些輕量級的數據,如果數據量比較大的情況下使用“ViewModel”在Fragment之間共享數據。被傳遞的數據需要在Destination上配置,配置方法有兩種,可以使用IDE提供的圖形界面進行配置也可以使用源碼的方式直接編輯Navigation Graph源文件實現。這里我們的目標Destination(CameraSettingFragment)需要一個integer類型的“camera_id”參數,配置完成后文件內容如下:

<?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/setting_nav_graph"
            app:startDestination="@id/mainSettingFragment">

    <fragment
            android:id="@+id/mainSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.MainSettingFragment"
            android:label="fragment_main_setting"
            tools:layout="@layout/fragment_main_setting">
        <action
                android:id="@+id/action_mainSettingFragment_to_cameraSettingFragment"
                app:destination="@id/cameraSettingFragment"/>
    </fragment>
    <fragment
            android:id="@+id/cameraSettingFragment"
            android:name="com.wangqiang.pro.navigationdemo.CameraSettingFragment"
            android:label="fragment_camera_setting"
            tools:layout="@layout/fragment_camera_setting">
        <argument
                android:name="camera_id"
                app:argType="integer"
                android:defaultValue="0" />
    </fragment>
</navigation>

完成后IDE應該是自動幫我們生成MainSettingFragmentDirections和CameraSettingFragmentArgs兩個類(如果沒有生成手動編譯一下工程),這兩個類的命名規則是分別在起始Des 提 nation和目標Destination加上“Directions”和“Args”后綴,他們的作用分別如下:

  • MainSettingFragmentDirections -- 設置要傳遞的參數
  • CameraSettingFragmentArgs -- 取出傳遞的參數

然后修改下我們的跳轉代碼:

//MainSettingFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    
    view.findViewById<Button>(R.id.camera).setOnClickListener {
        val action = MainSettingFragmentDirections.actionMainSettingFragmentToCameraSettingFragment().setCameraId(1)
        findNavController().navigate(action)
    }
}

取出傳遞參數:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val cameraId = CameraSettingFragmentArgs.fromBundle(arguments).cameraId
}

到此為止頁面間傳值實現完成,如果不生效或發生錯誤請檢查依賴組件是否正確配置。這里只是簡略的說明下該框架的使用方法,作為對該框架使用流程的備忘,還有很多細節的地方沒有涉及到,如果需要請自行查閱官方文檔。

總結分析



用如此優雅的方式重新定義Android App中的頁面導航,在這里我獻上在認知范圍內的所有贊美,Navigation框架的出現確實為Android App開發過程中那謎一樣的跳轉帶來了光明與秩序。下面是我個人對Navigation Architecture Component粗鄙的認知與理解,如有不到位的地方歡迎留言(拍磚)指正。Navigation Architecture Component中主要有以下核心類組成,主要關系如圖:

image.png

該圖所對應的Navigation Architecture Component版本為1.0.0-alpha07由于是alpha版本所以版本之間的變化可以可能會有比較大的變化各位看官請注意。
我認為關于Navigation最核心的東西就是以上這些了,下面說下我的個人理解。先來看下NaviHostFragment的源碼

public class NavHostFragment extends Fragment implements NavHost {
  private NavController mNavController;
   @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Context context = requireContext();

        mNavController = new NavController(context);
        mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
      ....
    }
  
    @NonNull
    @Override
    public NavController getNavController() {
        if (mNavController == null) {
            throw new IllegalStateException("NavController is not available before onCreate()");
        }
        return mNavController;
    }
}

public interface NavHost {
    @NonNull
    NavController getNavController();
}

源碼比較簡單只有三百行左右的,這里只摘取了用于說明問題的關鍵代碼,主要作用就是作為其它的功能頁面(Fragment)的宿主(容器),實現功能頁面的切換。前面Demo中在Activity中的xml布局文件中寫的fragment標簽就是它(NavHostFragment)。NahHostFragment里面有一個mNavController實例變量同時實現了一個NavHost的接口,這個接口只有一個getNavController方法其主要作用就是用于獲取NavHostFragment的私有變量 mNavController,關于NavController的源碼如下:

public class NavController {
  final Deque<NavDestination> mBackStack = new ArrayDeque<>();
  private final SimpleNavigatorProvider mNavigatorProvider = new SimpleNavigatorProvider() {
        @Nullable
        @Override
        public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
                @NonNull Navigator<? extends NavDestination> navigator) {
            Navigator<? extends NavDestination> previousNavigator =
                    super.addNavigator(name, navigator);
            if (previousNavigator != navigator) {
                if (previousNavigator != null) {
                    previousNavigator.removeOnNavigatorNavigatedListener(mOnNavigatedListener);
                }
                navigator.addOnNavigatorNavigatedListener(mOnNavigatedListener);
            }
            return previousNavigator;
        }
  };
  
  public NavController(@NonNull Context context) {
        mContext = context;
        while (context instanceof ContextWrapper) {
            if (context instanceof Activity) {
                mActivity = (Activity) context;
                break;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        mNavigatorProvider.addNavigator(new NavGraphNavigator(mContext));
        mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
    }
}

NavController的主要是用于控制頁面(Fragment, Activity, NavGraph)的切換,主要有兩個實例變量需要注意分別是mBackStack和mNavigatorProvider。加載到NavHostFragment中的頁面(Destination)的棧存儲結構就是通過mBackStack去記錄維護。mNavigatorProvider是一個導航(跳轉)策略集合,為什么要這樣搞?個人覺得這里設計的就比較巧妙,這是因為同時支持Fragment,Activity和NavGraph導航(跳轉)而這三種Destination的跳轉方式并不一樣,所以通過這種設計方法就可以支持多種跳轉策略,這個策略集合默認添加了ActivityNavigator、NavGraphNavigator和FragmentNavigator。細心的你可能發現上面的源碼沒有FragmentNavigator,??對上面確實沒有,因為它不是在NavController實例化的時候添加的,它是是在NavHostFragment初始化的時候通過外部注冊的方式添加的。理解了這一點,你就可以靈活的對Navigation框架的跳轉策略進行擴展,例如你想對框架增加View之間路由(跳轉)的擴展!怎么搞?你只需要寫一個繼承Navigator的ViewNavigator。

Navigator是什么?Action是對一個導航(或者說跳轉)動作的描述,而Navigator就是Action的具體執行者,這是我能想到的對Navigator的最簡潔的描述。關于Navigator的核心內容如下:

public abstract class Navigator<D extends NavDestination> {

    @Retention(RUNTIME)
    @Target({TYPE})
    @SuppressWarnings("UnknownNullness") // TODO https://issuetracker.google.com/issues/112185120
    public @interface Name {
        String value();
    }

    @Retention(SOURCE)
    @IntDef({BACK_STACK_UNCHANGED, BACK_STACK_DESTINATION_ADDED, BACK_STACK_DESTINATION_POPPED})
    @interface BackStackEffect {}
  
    private final CopyOnWriteArrayList<OnNavigatorNavigatedListener> mOnNavigatedListeners =
            new CopyOnWriteArrayList<>();
  
    @NonNull
    public abstract D createDestination();
  
      public abstract void navigate(@NonNull D destination, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Extras navigatorExtras);
}

關于Action的實現類叫NavAction實現很簡單只有幾十行代碼,感興趣可以自己看這里不做贅述。

首先看到Navigator中定義了兩個注解分別是Name和BackStackEffect,作用如下:

  • Name 該注解的作用是用于自定義注冊到NavigatorProvider的名稱,繼承Navigator的子類必須使用該注解標注。
  • BackStackEffect 該注解的作用類似android support包中的@IdRes注解,用于限定變量的取值范圍(BACK_STACK_UNCHANGED, BACK_STACK_DESTINATION_ADDED, BACK_STACK_DESTINATION_POPPED),用于編譯階段的檢查,在OnNavigatorNavigatedListener.onNavigatorNavigated()中有用到。

然后還定義了一個抽象的navigate(...)方法,在執行Destination間跳轉的時候就是調用該方法,對應的ActivityNavigator、FragmentNavigator、GraphNavigator分別有不同的具體實現。這里我們可以看下FragmentNavigator的具體實現:

//定義注冊到NavigatorProvider中的名稱
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
  @Override
    public void 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;
        }
        final Fragment frag = destination.createFragment(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);

        final @IdRes int destId = destination.getId();
        final boolean initialNavigation = mBackStack.isEmpty();
        final boolean isClearTask = navOptions != null && navOptions.shouldClearTask();
        // TODO Build first class singleTop behavior for fragments
        final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
                && navOptions.shouldLaunchSingleTop()
                && mBackStack.peekLast() == destId;

        int backStackEffect;
        if (initialNavigation || isClearTask) {
            backStackEffect = BACK_STACK_DESTINATION_ADDED;
        } else if (isSingleTopReplacement) {
            // Single Top means we only want one instance on the back stack
            if (mBackStack.size() > 1) {
                // If the Fragment to be replaced is on the FragmentManager's
                // back stack, a simple replace() isn't enough so we
                // remove it from the back stack and put our replacement
                // on the back stack in its place
                mFragmentManager.popBackStack();
                ft.addToBackStack(Integer.toString(destId));
                mIsPendingBackStackOperation = true;
            }
            backStackEffect = BACK_STACK_UNCHANGED;
        } else {
            ft.addToBackStack(Integer.toString(destId));
            mIsPendingBackStackOperation = true;
            backStackEffect = BACK_STACK_DESTINATION_ADDED;
        }
        if (navigatorExtras instanceof Extras) {
            Extras extras = (Extras) navigatorExtras;
            for (Map.Entry<View, String> sharedElement : extras.getSharedElements().entrySet()) {
                ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
            }
        }
        ft.setReorderingAllowed(true);
        ft.commit();
        // The commit succeeded, update our view of the world
        if (backStackEffect == BACK_STACK_DESTINATION_ADDED) {
            mBackStack.add(destId);
        }
        dispatchOnNavigatorNavigated(destId, backStackEffect);
    }
}

巴拉巴拉很長一坨,主要的就是通過FragmentManager完成Fragment Destination的切換,剩下的就是為切換過程增加動畫效果以及為代切換的Fragment設置屬性以及切換數據等等。

Destination對應的實現類是NavDestination,主要有3個直接子類NavGraph、FragmentNavigator.Destination和ActivityNavigator.Destination,分別用于對導航圖,Fragment和Activity的描述。多個Destination就組成了NavGraph,這里需要注意下NavGraph的子節點也可以是一個NavGraph,而且節點間可以任意調轉,這說明NavGraph的數據結構是圖。下面看下NavDestination的源碼:

public class NavDestination {
    //跳轉策略
    private final Navigator mNavigator;
    private NavGraph mParent;
    private int mId;
    private CharSequence mLabel;
    //跳轉需要的參數
    private Bundle mDefaultArgs;
    private ArrayList<NavDeepLink> mDeepLinks;
    private SparseArrayCompat<NavAction> mActions;
  
    public void navigate(@Nullable Bundle args, @Nullable NavOptions navOptions,
            @Nullable Navigator.Extras navigatorExtras) {
        Bundle defaultArgs = getDefaultArguments();
        Bundle finalArgs = new Bundle();
        finalArgs.putAll(defaultArgs);
        if (args != null) {
            finalArgs.putAll(args);
        }
        mNavigator.navigate(this, finalArgs, navOptions, navigatorExtras);
    }
}

NavDestination有一個mNavigator實例變量用于存儲跳轉策略,因為前面說過NavDestination有多個類型(子類),不同類型的NavDestination之間的跳轉策略是不一樣的,NavDestination中的navigate(...)方法最終就是把跳轉工作委托給了mNavigator,我通過NavController執行跳轉的時候最終就是調用到了這里。mActions是當前節點可以導航(跳轉)到哪些節點的一個集合,是(1: n)的關系,典型的圖數據結構。

最后感謝Google賜我Navigation框架!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,155評論 3 425
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,255評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,646評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,838評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,399評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,146評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,338評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,565評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,059評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,296評論 2 376

推薦閱讀更多精彩內容