Google 在官方開發(fā)工具包中(Android Jetpack)中提供了一個(gè)用于Android app導(dǎo)航的全新框架“Navigation”霎肯,配合IDE可以很方便的查看App中頁(yè)面之間或模塊之間的關(guān)聯(lián)關(guān)系檬输,這個(gè)跟IOS中StoryBoard很像辆琅。
官方文檔:The Navigation Architecture Component
官方教程:Navigation Codelab
官方Demo:android-navigation
概述
使用Navigation Architecture Component(后面簡(jiǎn)稱Navigation)不但可以實(shí)現(xiàn)App間復(fù)雜的導(dǎo)航關(guān)系而且還使得導(dǎo)航關(guān)系可視化在這一點(diǎn)上要比一些第三方的導(dǎo)航框架(ARouter等)要好的多焚刚。在Navigation框架中引入了以下幾個(gè)概念需要說明下:
Destination -- 直譯過來就是目的地的意思越庇,結(jié)合Android開發(fā)環(huán)境理解鞠柄,指的就是頁(yè)面或者模塊等骄瓣。Activity、Fragment师坎、Graph等都可以充當(dāng)一個(gè)Destination恕酸。
- Action -- 就是頁(yè)面間的導(dǎo)航關(guān)系用于連接Destination
- Graph -- 多個(gè)Destination通過Action連接起來就是一個(gè)Graph
Navigation 框架支持在Fragment、Activity胯陋、Graph蕊温、SubGraph、自定義Destination之間導(dǎo)航遏乔。包括前面提到的功能總結(jié)起來Navigation框架總共提供了以下一系列的附加功能义矛,用于輔助開發(fā)簡(jiǎn)化開發(fā)流程:
- 處理Fragment的事務(wù)(Transactions)
- 為返回操作(Back & Up)提供正確的默認(rèn)實(shí)現(xiàn)
- 為動(dòng)畫和過渡提供標(biāo)準(zhǔn)的資源
- 支持Deep link
- 通過很少的額外操作就可以支持Navigation UI,例如Navigation Drawer按灶、Bottom Navigation等
- 使頁(yè)面間傳值變的更加安全
- 通過IDE可以實(shí)現(xiàn)可視化編輯
在使用Navigation框架的時(shí)候有以下幾點(diǎn)需要注意:
- 使用一個(gè)棧來代表App的導(dǎo)航狀態(tài)
- 必須要有一個(gè)固定的起始Destination
- 不能使用Up button退出你的程序
- 在App任務(wù)中向上和返回按鈕是等價(jià)的
- 深度鏈接到目標(biāo)或?qū)Ш降较嗤哪繕?biāo)應(yīng)產(chǎn)生相同的堆棧
Navigation的使用
配置IDE
要是使用Navigation框架要求你的Android Studio版本必須是3.2+症革,如果你的Android Studio版本是3.2,你需要進(jìn)入IDE的設(shè)置界面找到“Enable Navigation Editor”選項(xiàng)并選中(需要重新啟動(dòng)Android Studio)鸯旁。
配置項(xiàng)目
創(chuàng)建一個(gè)標(biāo)準(zhǔn)的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"
創(chuàng)建Navigation Graph
關(guān)于這里官方教程有點(diǎn)啰嗦總結(jié)起來就是以下幾步:
- 在你項(xiàng)目工程的“res”目錄下創(chuàng)建“navigation”文件夾
- 在新建的“navigation”目錄下右鍵新建一個(gè)“Navigation resource file”
完成后預(yù)覽界面和源碼界面分別如下圖所示:
根據(jù)提示點(diǎn)擊"+"創(chuàng)建幾個(gè)測(cè)試用的Destination量蕊,完成后如圖:
對(duì)應(yīng)的源碼如下:
<?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>
頁(yè)面跳轉(zhuǎn)
要實(shí)現(xiàn)從MainSettingFragment到CameraSettingFragment我們只需要在MainSettingFragment中的按鈕點(diǎn)擊事件中添加如下代碼:
view.findViewById<Button>(R.id.camera)
.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_mainSettingFragment_to_cameraSettingFragment))
“action_mainSettingFragment_to_cameraSettingFragment”就是你定義的Action的id残炮。
頁(yè)面?zhèn)髦?/h1>
官方提倡通過這種方式傳遞一些輕量級(jí)的數(shù)據(jù)韭赘,如果數(shù)據(jù)量比較大的情況下使用“ViewModel”在Fragment之間共享數(shù)據(jù)。被傳遞的數(shù)據(jù)需要在Destination上配置势就,配置方法有兩種泉瞻,可以使用IDE提供的圖形界面進(jìn)行配置也可以使用源碼的方式直接編輯Navigation Graph源文件實(shí)現(xiàn)脉漏。這里我們的目標(biāo)Destination(CameraSettingFragment)需要一個(gè)integer類型的“camera_id”參數(shù),配置完成后文件內(nèi)容如下:
<?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應(yīng)該是自動(dòng)幫我們生成MainSettingFragmentDirections和CameraSettingFragmentArgs兩個(gè)類(如果沒有生成手動(dòng)編譯一下工程)袖牙,這兩個(gè)類的命名規(guī)則是分別在起始Des 提 nation和目標(biāo)Destination加上“Directions”和“Args”后綴侧巨,他們的作用分別如下:
- MainSettingFragmentDirections -- 設(shè)置要傳遞的參數(shù)
- CameraSettingFragmentArgs -- 取出傳遞的參數(shù)
然后修改下我們的跳轉(zhuǎn)代碼:
//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)
}
}
取出傳遞參數(shù):
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val cameraId = CameraSettingFragmentArgs.fromBundle(arguments).cameraId
}
到此為止頁(yè)面間傳值實(shí)現(xiàn)完成,如果不生效或發(fā)生錯(cuò)誤請(qǐng)檢查依賴組件是否正確配置鞭达。這里只是簡(jiǎn)略的說明下該框架的使用方法司忱,作為對(duì)該框架使用流程的備忘,還有很多細(xì)節(jié)的地方?jīng)]有涉及到畴蹭,如果需要請(qǐng)自行查閱官方文檔坦仍。
總結(jié)分析
用如此優(yōu)雅的方式重新定義Android App中的頁(yè)面導(dǎo)航,在這里我獻(xiàn)上在認(rèn)知范圍內(nèi)的所有贊美叨襟,Navigation框架的出現(xiàn)確實(shí)為Android App開發(fā)過程中那謎一樣的跳轉(zhuǎn)帶來了光明與秩序繁扎。下面是我個(gè)人對(duì)Navigation Architecture Component粗鄙的認(rèn)知與理解,如有不到位的地方歡迎留言(拍磚)指正糊闽。Navigation Architecture Component中主要有以下核心類組成梳玫,主要關(guān)系如圖:
該圖所對(duì)應(yīng)的Navigation Architecture Component版本為1.0.0-alpha07由于是alpha版本所以版本之間的變化可以可能會(huì)有比較大的變化各位看官請(qǐng)注意。
我認(rèn)為關(guān)于Navigation最核心的東西就是以上這些了墓怀,下面說下我的個(gè)人理解汽纠。先來看下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();
}
源碼比較簡(jiǎn)單只有三百行左右的,這里只摘取了用于說明問題的關(guān)鍵代碼傀履,主要作用就是作為其它的功能頁(yè)面(Fragment)的宿主(容器)虱朵,實(shí)現(xiàn)功能頁(yè)面的切換。前面Demo中在Activity中的xml布局文件中寫的fragment標(biāo)簽就是它(NavHostFragment)钓账。NahHostFragment里面有一個(gè)mNavController實(shí)例變量同時(shí)實(shí)現(xiàn)了一個(gè)NavHost的接口碴犬,這個(gè)接口只有一個(gè)getNavController方法其主要作用就是用于獲取NavHostFragment的私有變量 mNavController,關(guān)于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的主要是用于控制頁(yè)面(Fragment, Activity, NavGraph)的切換梆暮,主要有兩個(gè)實(shí)例變量需要注意分別是mBackStack和mNavigatorProvider服协。加載到NavHostFragment中的頁(yè)面(Destination)的棧存儲(chǔ)結(jié)構(gòu)就是通過mBackStack去記錄維護(hù)。mNavigatorProvider是一個(gè)導(dǎo)航(跳轉(zhuǎn))策略集合啦粹,為什么要這樣搞偿荷?個(gè)人覺得這里設(shè)計(jì)的就比較巧妙,這是因?yàn)橥瑫r(shí)支持Fragment唠椭,Activity和NavGraph導(dǎo)航(跳轉(zhuǎn))而這三種Destination的跳轉(zhuǎn)方式并不一樣胚宦,所以通過這種設(shè)計(jì)方法就可以支持多種跳轉(zhuǎn)策略羔味,這個(gè)策略集合默認(rèn)添加了ActivityNavigator蜜葱、NavGraphNavigator和FragmentNavigator祠够。細(xì)心的你可能發(fā)現(xiàn)上面的源碼沒有FragmentNavigator,??對(duì)上面確實(shí)沒有,因?yàn)樗皇窃贜avController實(shí)例化的時(shí)候添加的斗塘,它是是在NavHostFragment初始化的時(shí)候通過外部注冊(cè)的方式添加的赢织。理解了這一點(diǎn),你就可以靈活的對(duì)Navigation框架的跳轉(zhuǎn)策略進(jìn)行擴(kuò)展馍盟,例如你想對(duì)框架增加View之間路由(跳轉(zhuǎn))的擴(kuò)展于置!怎么搞?你只需要寫一個(gè)繼承Navigator的ViewNavigator朽合。
Navigator是什么俱两?Action是對(duì)一個(gè)導(dǎo)航(或者說跳轉(zhuǎn))動(dòng)作的描述,而Navigator就是Action的具體執(zhí)行者曹步,這是我能想到的對(duì)Navigator的最簡(jiǎn)潔的描述宪彩。關(guān)于Navigator的核心內(nèi)容如下:
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);
}
關(guān)于Action的實(shí)現(xiàn)類叫NavAction實(shí)現(xiàn)很簡(jiǎn)單只有幾十行代碼,感興趣可以自己看這里不做贅述讲婚。
首先看到Navigator中定義了兩個(gè)注解分別是Name和BackStackEffect尿孔,作用如下:
- Name 該注解的作用是用于自定義注冊(cè)到NavigatorProvider的名稱,繼承Navigator的子類必須使用該注解標(biāo)注筹麸。
- BackStackEffect 該注解的作用類似android support包中的@IdRes注解活合,用于限定變量的取值范圍(BACK_STACK_UNCHANGED, BACK_STACK_DESTINATION_ADDED, BACK_STACK_DESTINATION_POPPED),用于編譯階段的檢查物赶,在OnNavigatorNavigatedListener.onNavigatorNavigated()中有用到白指。
然后還定義了一個(gè)抽象的navigate(...)方法,在執(zhí)行Destination間跳轉(zhuǎn)的時(shí)候就是調(diào)用該方法酵紫,對(duì)應(yīng)的ActivityNavigator告嘲、FragmentNavigator、GraphNavigator分別有不同的具體實(shí)現(xiàn)奖地。這里我們可以看下FragmentNavigator的具體實(shí)現(xiàn):
//定義注冊(cè)到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);
}
}
巴拉巴拉很長(zhǎng)一坨橄唬,主要的就是通過FragmentManager完成Fragment Destination的切換,剩下的就是為切換過程增加動(dòng)畫效果以及為代切換的Fragment設(shè)置屬性以及切換數(shù)據(jù)等等参歹。
Destination對(duì)應(yīng)的實(shí)現(xiàn)類是NavDestination仰楚,主要有3個(gè)直接子類NavGraph、FragmentNavigator.Destination和ActivityNavigator.Destination犬庇,分別用于對(duì)導(dǎo)航圖僧界,F(xiàn)ragment和Activity的描述。多個(gè)Destination就組成了NavGraph臭挽,這里需要注意下NavGraph的子節(jié)點(diǎn)也可以是一個(gè)NavGraph捎泻,而且節(jié)點(diǎn)間可以任意調(diào)轉(zhuǎn),這說明NavGraph的數(shù)據(jù)結(jié)構(gòu)是圖埋哟。下面看下NavDestination的源碼:
public class NavDestination {
//跳轉(zhuǎn)策略
private final Navigator mNavigator;
private NavGraph mParent;
private int mId;
private CharSequence mLabel;
//跳轉(zhuǎn)需要的參數(shù)
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有一個(gè)mNavigator實(shí)例變量用于存儲(chǔ)跳轉(zhuǎn)策略,因?yàn)榍懊嬲f過NavDestination有多個(gè)類型(子類),不同類型的NavDestination之間的跳轉(zhuǎn)策略是不一樣的赤赊,NavDestination中的navigate(...)方法最終就是把跳轉(zhuǎn)工作委托給了mNavigator闯狱,我通過NavController執(zhí)行跳轉(zhuǎn)的時(shí)候最終就是調(diào)用到了這里。mActions是當(dāng)前節(jié)點(diǎn)可以導(dǎo)航(跳轉(zhuǎn))到哪些節(jié)點(diǎn)的一個(gè)集合抛计,是(1: n)的關(guān)系哄孤,典型的圖數(shù)據(jù)結(jié)構(gòu)。
最后感謝Google賜我Navigation框架吹截!