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
关于这里官方教程有点啰嗦总结起来就是以下几步:
- 在你项目工程的“res”目录下创建“navigation”文件夹
- 在新建的“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框架!