Android Navigation 如何动态的更换StartDestination &&保存Fragment状态

Navigation使用方法

1. 创建navigation

首先在我们Module下的res 右键,创建Android Resource Directory,选择navigation,就创建了一个navigation目录,

image-20201013181454671.png

然后在目录下右键,创建navigation 资源文件

image-20201013181929598.png

然后在资源文件里就可以编辑Fragment的跳转规则了

<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/nav_graph"
 app:startDestination="@id/blankFragment"
 tools:ignore="UnusedNavigation">
​
 <fragment
 android:id="@+id/blankFragment"
 android:name="com.hk.module.login.ui.fragment.BlankFragment"
 android:label="fragment_blank"
 tools:layout="@layout/login_fragment_blank" />
 <fragment
 android:id="@+id/inputPasswordFragment"
 android:name="com.hk.module.login.ui.fragment.InputPasswordFragment"
 android:label="InputPasswordFragment" >
 <action
 android:id="@+id/action_inputPasswordFragment_to_inputCodeFragment"
 app:destination="@id/inputCodeFragment" />
 </fragment>
 <fragment
 android:id="@+id/inputPhoneFragment"
 android:name="com.hk.module.login.ui.fragment.InputPhoneFragment"
 android:label="InputPhoneFragment" >
 <action
 android:id="@+id/action_inputPhoneFragment_to_inputPasswordFragment"
 app:destination="@id/inputPasswordFragment" />
 <action
 android:id="@+id/action_inputPhoneFragment_to_inputCodeFragment"
 app:destination="@id/inputCodeFragment" />
 </fragment>
 <fragment
 android:id="@+id/inputCodeFragment"
 android:name="com.hk.module.login.ui.fragment.InputCodeFragment"
 android:label="InputCodeFragment" >
 <action
 android:id="@+id/action_inputCodeFragment_to_setPasswordFragment"
 app:destination="@id/setPasswordFragment" />
 <action
 android:id="@+id/action_inputCodeFragment_to_bindWeChatFragment"
 app:destination="@id/bindWeChatFragment" />
 </fragment>
 <fragment
 android:id="@+id/bindWeChatFragment"
 android:name="com.hk.module.login.ui.fragment.BindWeChatFragment"
 android:label="BindWeChatFragment" >
 <action
 android:id="@+id/action_bindWeChatFragment_to_changeBindFragment"
 app:destination="@id/changeBindFragment" />
 </fragment>
 <fragment
 android:id="@+id/changeBindFragment"
 android:name="com.hk.module.login.ui.fragment.ChangeBindFragment"
 android:label="ChangeBindFragment" />
 <fragment
 android:id="@+id/setPasswordFragment"
 android:name="com.hk.module.login.ui.fragment.SetPasswordFragment"
 android:label="SetPasswordFragment" />
</navigation>

这里涉及到3个标签标签:navigation 不用说,fragment标签 就是表示这是个Fragment 这里还可以是其他标签,这个下面再说,第三个标签是 action 就是定义的 "动作"

navigation 必须指定一个 startDestination 表示 整个页面的起始Fragment

fragment中的name标签为 要跳转的Fragment的全路径类名

action 中的destination 表示要跳转的 Fragment 的id

navigation创建好了,就要和Activity进行关联了,有两种方式,第一种是在xml里关联,一种是在代码里关联

先说第一种

首先 在Activity的 xml中添加fragment标签

 <fragment
 android:id="@+id/nav_host_fragment"
 android:name="androidx.navigation.fragment.NavHostFragment"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 app:navGraph="@navigation/login_nav_graph"
 app:defaultNavHost="true" />

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

image-20201016145847537.png

我们这里只用到了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<Integer> 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<View, String> 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

 <fragment
 android:id="@+id/nav_host_fragment"
 android:name="com.XXX.XXX.KeepStateNavHostFragment"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 app:navGraph="@navigation/login_nav_graph"
 app:defaultNavHost="true" />

在路由的时候这样使用即可,就是把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中的一个组件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358