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)。
配置項目
創建一個標準的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
關于這里官方教程有點啰嗦總結起來就是以下幾步:
- 在你項目工程的“res”目錄下創建“navigation”文件夾
- 在新建的“navigation”目錄下右鍵新建一個“Navigation resource file”
完成后預覽界面和源碼界面分別如下圖所示:
根據提示點擊"+"創建幾個測試用的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/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中主要有以下核心類組成,主要關系如圖:
該圖所對應的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框架!