Navigation使用方法
1. 创建navigation
首先在我们Module下的res 右键,创建Android Resource Directory,选择navigation,就创建了一个navigation目录,
然后在目录下右键,创建navigation 资源文件
然后在资源文件里就可以编辑Fragment的跳转规则了
这里涉及到3个标签标签:navigation 不用说,fragment标签 就是表示这是个Fragment 这里还可以是其他标签,这个下面再说,第三个标签是 action 就是定义的 "动作"
navigation 必须指定一个 startDestination 表示 整个页面的起始Fragment
fragment中的name标签为 要跳转的Fragment的全路径类名
action 中的destination 表示要跳转的 Fragment 的id
navigation创建好了,就要和Activity进行关联了,有两种方式,第一种是在xml里关联,一种是在代码里关联
先说第一种
首先 在Activity的 xml中添加fragment标签
name 写死的,指定整个navigation的hostFragment,下面讲到怎么自定义Navigator再说整个参数;navGraph 指定的是 在navigation中写的 xml;defaultNavHost 就是这个意思,没啥好解释的,我也解释不清。
第二种参考参考下面的自定义startDestination
2. 使用
在使用上我遇到了两大难题,其一是:我的起始Fragment是动态的,并不一定是xml中指定的 startDestination,其二是 navigation pop 和 push的时候 对Fragment的 操作是 replace,所以会导致生命周期重新走一遍,在注册登录这个过程中,这种情况显然不可以,下面先说一下基本的使用方法,再说一下我是怎么解决这两个问题的。
其实Navigation使用很简单,navigation和activity(确切的说是Fragment)绑定之后,使用两个方法就行,一个是navigate,就是跳转,一个是navigateUp,就是返回。
如果想要跳转到新页面时,在Fragment中使用:
NavHostFragment.findNavController(this).navigate(destinationID, bundle);
this 是当前的fragment,destinationID 就是要跳转的action id ,这个action对应的fragment一定得是当前的Fragment, bundle 是带过去的参数,内部是调用的 setArguments。就是这么方便,太简单了吧。
使用navigation默认是会添加到返回栈的,如果想要返回上一级怎么办
NavHostFragment.findNavController(this).navigateUp();
是不是也是非常简单。
基本使用方法就是这样,下面看上面遇到的两个方法怎么解决。
2.1 动态改变startDestination
startDestination 对应的其实就是在navigation中定义的 fragment的id,比如我APP是自动获取手机号的,那获取完手机号之后,第一个页面就是输入密码,如果获取手机号失败,第一个页面就是输入手机号,怎么换呢。
Bundle args = new Bundle();
int startDestinationID; // 起始Fragment
int partnerType; // 业务Type
switch (mType) {
...
default:
partnerType = mType;
startDestinationID = R.id.inputPhoneFragment;
}
args.putInt(Const.BundleKey.TYPE, partnerType);
args.putParcelable(Const.BundleKey.OBJECT, mMiddlewareModel);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavInflater navInflater = navController.getNavInflater();
NavGraph navGraph = navInflater.inflate(R.navigation.login_nav_graph);
navGraph.setStartDestination(startDestinationID);
navController.setGraph(navGraph, args);
第一步: 先确定我们需要跳哪个Fragment,就是先确定一个startDestinationID
第二步: 然后通过Navigation.findNavController(this, R.id.nav_host_fragment) 找到 NavController 实例,R.id.nav_host_fragment 这个id是activity中指定的fragment的id,NavController 可以通过Fragment拿到,两者有着很密切的联系,而且NavController 是在push和pop时使用最多的类。
第三步:通过 NavController 得到NavInflater 实例,然后将我们在 navigation里写的xml文件加载出来,有点类似于layout的加载,然后就可以给 NavGraph设置startDestinationID 了, 然后再把navGraph 和要传的参数,设置给navController。
这里的第二步和第三步,也是activity关联navigation的方法。
通过这种方法,不仅可以动态的设置startDestination 也可以动态的设置启动参数。
2.2 切换时使Fragment保存状态
当我们使用NavHostFragment 的时候通过 NavHostFragment.findNavController 拿到的是 NavController,navigation 和navigationUp就是NavController 中的方法,但是具体的路由实现是由一个个navigator完成的
在源码中给提供了这些navigator
我们这里只用到了FragmentNavigator 其他的再研究,其中KeepStateFragmentNavigator就是我们自定义的。
首先看一下FragmentNavigator 在切换时为什么不能保存状态,源码是这样的:
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
...
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
...
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
在进行跳转时 直接使用了replace,所以导致当前页面会调用 onDestroyView,即fragment变为 inactive,当进行pop操作时,fragment重新进入 active状态时,会重新调用 onViewCreated 等方法,导致页面重新绘制,其实在这种情况下,我们可以直接用ViewModel和LiveData对数据进行保存,但是这次想尝试一下新的解决办法。在知道原因后就好办了,直接继承FragmentNavigator 把方法重写了不就行了,我确实也是这样做的。
上代码:
@Navigator.Name("keepFragment")
public class KeepStateFragmentNavigator extends FragmentNavigator {
private static final String TAG = "KeepStateFragmentNavigator";
private ArrayDeque mBackStack = new ArrayDeque<>();
private final FragmentManager mFragmentManager;
private final int mContainerId;
private Context mContext;
public KeepStateFragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager, int containerId) {
super(context, manager, containerId);
mFragmentManager = manager;
mContainerId = containerId;
mContext = context;
}
@Override
public void onRestoreState(@Nullable Bundle savedState) {
if (savedState != null) {
int[] backStack = savedState.getIntArray("androidx-nav-fragment:navigator:backStackIds");
if (backStack != null) {
mBackStack.clear();
for (int destId : backStack) {
mBackStack.add(destId);
}
}
}
}
@Override
@Nullable
public Bundle onSaveState() {
Bundle b = new Bundle();
int[] backStack = new int[mBackStack.size()];
int index = 0;
for (Integer id : mBackStack) {
backStack[index++] = id;
}
b.putIntArray("androidx-nav-fragment:navigator:backStackIds", backStack);
return b;
}
@SuppressLint("LongLogTag")
@Override
public boolean popBackStack() {
if (mBackStack.isEmpty()) {
return false;
}
if (mFragmentManager.isStateSaved()) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already"
+ " saved its state");
return false;
}
mFragmentManager.popBackStack(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
mBackStack.removeLast();
return true;
}
@SuppressLint("LongLogTag")
@Nullable
@Override
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 tag = String.valueOf(destination.getId());
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
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);
}
final Fragment currentFragment = mFragmentManager.getPrimaryNavigationFragment();
if (currentFragment != null) {
ft.hide(currentFragment);
}
Fragment frag = mFragmentManager.findFragmentByTag(tag);
if (frag == null) {
frag = mFragmentManager.getFragmentFactory().instantiate(mContext.getClassLoader(), className);
frag.setArguments(args);
ft.add(mContainerId, frag, tag);
} else {
ft.show(frag);
}
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
if (initialNavigation) {
isAdded = true;
} 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(
generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
ft.addToBackStack(generateBackStackName(mBackStack.size(), destId));
}
isAdded = false;
} else {
ft.addToBackStack(generateBackStackName(mBackStack.size() + 1, destId));
isAdded = true;
}
if (navigatorExtras instanceof Extras) {
Extras extras = (Extras) navigatorExtras;
for (Map.Entry sharedElement : extras.getSharedElements().entrySet()) {
ft.addSharedElement(sharedElement.getKey(), sharedElement.getValue());
}
}
ft.setReorderingAllowed(true);
ft.commitAllowingStateLoss();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
@NonNull
private String generateBackStackName(int backStackIndex, int destId) {
return backStackIndex + "-" + destId;
}
}
代码有点长,其实有用的只有 fragment处理的那一部分,这里大部分代码都是在处理返回栈,其实可以直接通过反射,拿到父类的mBackStack,只重写navigate 方法就行。
在自己做项目的时候走了弯路,参考了NavHostFragment,发现了这个方法:
@Deprecated
@NonNull
protected Navigator extends FragmentNavigator.Destination> createFragmentNavigator() {
return new FragmentNavigator(requireContext(), getChildFragmentManager(),
getContainerId());
}
于是就继承了NavHostFragment 写了一个
public class KeepStateNavHostFragment extends NavHostFragment {
@NonNull
@Override
protected Navigator extends FragmentNavigator.Destination> createFragmentNavigator() {
return new KeepStateFragmentNavigator(requireContext(), getChildFragmentManager(),
getContainerId());
}
protected int getContainerId() {
int id = getId();
if (id != 0 && id != View.NO_ID) {
return id;
}
// Fallback to using our own ID if this Fragment wasn't added via
// add(containerViewId, Fragment)
return R.id.nav_host_fragment_container;
}
}
然在xml中 把 NavHostFragment 换成KeepStateNavHostFragment
在路由的时候这样使用即可,就是把NavHostFragment 换成KeepStateNavHostFragment
KeepStateNavHostFragment.findNavController(this).navigate(destinationID, bundle);
代码很简单,但是在我写这篇文档的时候又发现了一个比较简单的方法
可以通过NavController 来添加任何navigation,这个方法就比较简单明了,注意KeepStateFragmentNavigator构造方法中的参数,第一个是Activity,第二个是hostFragment的childFragment,这个一定不能是activity的,否则会有bug,第三个则是Fragment 的id。
使用方法和默认的navigation一致
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
Fragment navHostFragment = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
navController.getNavigatorProvider().addNavigator(new KeepStateFragmentNavigator(this, navHostFragment.getChildFragmentManager(), R.id.nav_host_fragment));
NavHostFragment.findNavController(this).navigate(destinationID, bundle);
至此,又学会了JetPack中的一个组件。