阅读 366

Jetpack 系列—— Navigation 实现原理篇

上篇传送门:Jetpack系列—— Navigation 使用入门篇

前言

在上一篇 Navigation 使用篇中详细讲解了如何接入和使用 Navigation 框架,如果您还未实际使用过该框架,本篇可能并不适于直接阅读。本篇源码分析基于 Navigation 2.3.5 版本。

最新版本为 2.4.0 ,对 多返回栈 提供官方支持,但还处于 alpha 版本。Navigation 全部历史版本可参考 版本更新记录

对于普通的开发者来说,如果不清楚技术背后的基本原理,当碰到一些奇怪的问题将无从下手。常见的,如果您已经接入了 Navigation 框架一定会有下面这些困惑:

  • 导航是如何完成的?为什么使用 navigation 跳转后,Fragment 的生命周期走到了 onDestroyView?这导致返回时重新触发 onCreateView。
  • 导航到 Activity 和 DialogFragment 是如何做到支持的?
  • 返回栈是自己管理吗?和 FragmentManager 的返回栈是什么关系?返回事件如何拦截?

带着种种疑问,本篇将带你深入理解 Navigation 背后的实现机制和设计理念,揭开 Navigation 神秘的面纱。

导航架构中的角色和设计原理

我们可以从 Navigation 设计者的角度出发,思考 Navigation 架构中的主要角色,这样有助于帮助你理解原理。

在上篇中讲到,我们使用 NavController 完成页面的跳转,也就是 NavController 是 导航框架暴露给上层的唯一接口,所有对于导航的操作都应该经由它直接或间接处理。

跳转是可能发生在任意页面的任意位置,因此,我们需要一个获取 NavController 接口,这个接口统一封装到了 Navigation 类中。

NavController 应该在何时创建呢?显然在布局阶段我们需要 Inflate 一个特殊的组件,在这个组件创建时完成 NavController 的创建,这个组件就是 NavHostFragment

接下来,我们考虑:由于 Navigation 设计为一个通用的“页面”跳转方案, 至于这个 “页面”的表现形式是一个 Activity 、一个 Fragment、一个对话框,还是其他任意形式,在框架层应该是不关心的,也就是框架需要对导航器本身做一个抽象,不同的导航器可以实现各自不同的导航和返回逻辑。

比如,Fragment 类型的导航器应当通过 FragmentManager 实现 Fragment 的显示与销毁;Activity 类型的导航器,应该使用 context 的 startActivity 完成跳转。这个导航器的抽象叫 Navigator

导航器多了就需要有一个导航器的工厂来负责根据类型创建对应的导航器,所以有了 NavigatorProvider

接下来,我们需要告诉系统所有可到达的目的地,为了更方便的管理目的地之间的关系,我们把相同的业务的目的地组合到同一个导航图中,导航图也可以作为目的地直接导航(也就是导航到起始目的地),这样目的地和导航图就组织成了一个树形结构,这个树形结构的根节点就是我们在布局文件中为 NavHostFragment 指定的 app:navGraph 属性。

我们把 导航图 定义为 NavGraph,目的地定义为 NavDestination

同时在导航的过程中需要确定导航终点、导航器类型、导航参数等信息,我们统一把它封装到 NavDestination 中。

最后,为了处理返回逻辑,我们还需要设计一个返回栈记录跳转的过程,栈中每一个元素定义为 NavBackStackEntry

结合上面分析,我们可以将 Navigation 架构的关系图画出来。

image.png

分析完 Navigation 大致的结构以后,我们开始看一看内部的实现细节。

导航控制器:NavController

通过前面分析,我们可以发现 NavController 是整个 Navigation 框架的核心,它将整体的导航细节封装到各个类型的 Navigator 中,自己管理记录和管理返回栈,对外暴露一些操作导航的 API。

接下来,我们看一下 NavController 是如何创建的。

NavController 的创建

当 NavHostFragment 走到 onCreate 生命周期时将 NavController 创建并完成初始化。

# NavHostFragment.java
public void onCreate(@Nullable Bundle savedInstanceState) {
    final Context context = requireContext();
    //创建
    mNavController = new NavHostController(context);
    //与NavHostFramgent生命周期绑定
    mNavController.setLifecycleOwner(this);
    //设置返回拦截
    mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());
    ...
}
复制代码

NavController 的查找

从上面 NavController 的创建可以发现, NavController 与 NavHostFragment 是一一对应的。

事实上,Navigation 内部就是为 NavHostFragment onCreateView方法返回的 view 打上一个 Tag,key 为一个固定ID,值为 onCreate 方法中创建的 NavController。

# NavHostFragment.java 
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    ...
    Navigation.setViewNavController(view, mNavController);
    ...
}

# Navigation.java 
public static void setViewNavController(@NonNull View view,
        @Nullable NavController controller) {
    view.setTag(R.id.nav_controller_view_tag, controller);
}
复制代码

接下来,查找过程就简单了,还记得 在 Navigation 使用篇中讲到,所有 findViewController() 最终都会走到以 view 为参数的重载方法。

# Navigation.java 
private static NavController findViewNavController(@NonNull View view) {
    while (view != null) {
        NavController controller = getViewNavController(view);
        if (controller != null) {
            return controller;
        }
        // 递归继续向上查找
        ViewParent parent = view.getParent();
        view = parent instanceof View ? (View) parent : null;
    }
    return null;
}

private static NavController getViewNavController(@NonNull View view) {
    //寻找事先设定好的 Tag
    Object tag = view.getTag(R.id.nav_controller_view_tag);
    NavController controller = null;
    ...
    if (tag instanceof NavController) {
        controller = (NavController) tag;
    }
    return controller;
}
复制代码

总结来看,NavController 的查找过程就是在给定的 view 层级往上查找最近的 NavController。也就是说,如果项目中本身使用了 嵌套的 NavHostFragment,那么在使用 findViewNavController 方法时需指定合适的 view,避免获取到其他层级的 NavController

导航的原理

这一节我们来看 navigation 到底是如何实现页面跳转的。在开始讲解页面跳转前,我们需要详细解释一下 上面提到了两个重要的名词,NavDestination 和 Navigator。

目的地的抽象——NavDestination

我们在导航图 xml 中配置的各个节点最终都会转化成 NavDestination,由于有嵌套图的存在,所以最终会形成 NavDestination 的树形结构。

下面是 NavDestination 的类图,可以分为两大类:

NavDestination.png

NavGraph、NavDestination 二者的关系和 Android 中 View 与 ViewGroup 的关系类似,是一种组合模式。

  • NavGraph:对应导航图 xml 中 节点;它在导航图中是虚拟存在的,其内部维护了本导航图中支持的所有目的地。
  • NavDestination: 除 NavGraph 外的其他实际的目的地

而其他 DialogFragmentNavigator.Destination、 ActivityNavigator.Destination、 FragmentNavigator.Destination 可以看做是 View 的具体实现。

那么导航图是何时构建起来的呢?答案还是 NavHostFragment 的初始化。这里不贴代码,直接看时序图:

image.png

可见,对导航图的解析统一交由 NavInflate 实现,解析完成后调用 NavDestination 的 onInflate 完成 NavDestination 的初始配置。

比如,FragmentNavigator.Destination:

public void onInflate(Context context, AttributeSet attrs) {
    super.onInflate(context, attrs);
    TypedArray a = context.getResources().obtainAttributes(attrs,
            R.styleable.FragmentNavigator);
    //读取 fragment节点的 android:name 属性 
    String className = a.getString(R.styleable.FragmentNavigator_android_name);
    //设置为成员变量
    if (className != null) {
        setClassName(className);
    }
    a.recycle();
}
复制代码

这个 className 的作用就是将来在跳转时通过类名创建出对应的Fragment 入栈。

导航器的抽象——Navigator

前文提到 Navigator 的设计是为了将实际的导航动作抽象出来,那么其具体实现又有哪些呢?

Navigator.png

Navigator 为抽象类,其中抽象方法包括三个:跳转动作 navigate、回退动作 popBackStack、创建 NavDestination。

Navigation 内置了 四种常用的 导航器:

导航器xml对应的节点navigatepopBackStack
NavGraphNavigatornavigation、include继续执行该 graph 起始目的地的 navigatedo nothing (因为 NavGraph 本身并不发生实际的跳转)
FragmentNavigatorfragment使用 fm 的 replace 完成目的地展示执行 fm 的 popBackStack 完成事务的回退
DialogFragmentNavigatordialog调用 dialogFragment.show 显示目的地调用 dialogFragment.dismiss 移除目的
ActivityNavigatoractivity调用 startActivity 启动页面调用 finish 退出页面

这些默认导航器的注册同样在 NavHostFragment 的初始化过程就完成。

可以看到 Fragment 的默认实现是使用 replace 完成页面的展示,也就是先移除后添加。所以,如果 FragmentA 跳转到 FragmentB,那么就会触发 FragmentA 的 onDestoryView 方法,当重新返回到 FragmentA 时则会触发 onCreateView 方法。由于 View 被重建了,就需要小心 view 状态的保存。

如果上述导航图不能满足你现有的需求,可以自定义导航图,导航图的名字就是在 xml 中使用的节点名,同时自定义导航图上需要指定相同的名字作为注解。例如 fragment:

@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator {}
复制代码

完整的自定义 navigator 流程为:

  1. 自定义 Navigator,并使用 Name 注解声明名称。
  2. 在 graph xml 配置文件中 使用第一步的名称作为 tag。
  3. 自定义Navigator 实现 onInflate 方法解析自定义属性。
  4. 通过 NavigatorProvider.addNavigator(name, navigator) 注册自定义的 Navigator

注意第4步,需要去掉布局文件中的 app:navGraph 配置,使用代码 setGraph,并在这之前调用 NavigatorProvider 的 addNavigator 方法。

导航——跳转

现在我们可以着手了解用于跳转的 navigate 方法到底做了什么。

image.png

光重载的 navigate 就有十多个, 但无论使用哪种重载方法,最终都会调用到下面这个方法:

void navigate(NavDestination node, Bundle args,
            NavOptions navOptions,
            Navigator.Extras navigatorExtras)
复制代码

通常我们传递的首个参数为 actionId,在此之前会通过遍历 NavGraph 查找当前导航图中对应的 NavDestination。

这里的参数列表:

  • node 需要导航的目的地
  • args 跳转携带的参数
  • navOptions 跳转的配置项,如转场动画、popupto、popUpToInclusive、launchSingleTop 属性等。
  • navigatorExtras 其他额外参数,默认实现支持 Fragment 跳转时共享元素(shareElement)、Activity 导航添加 Flag 参数。

该方法的实现很长,我们挑出重点看下:

# NavController.java

private void navigate(NavDestination node, Bundle args, NavOptions navOptions, Navigator.Extras navigatorExtras) {
    // 处理 popUpTo 属性,也就是先 pop 再跳转
    boolean popped = false;
    boolean launchSingleTop = false;
    if (navOptions != null) {
        if (navOptions.getPopUpTo() != -1) {
            popped = popBackStackInternal(navOptions.getPopUpTo(),
                    navOptions.isPopUpToInclusive());
        }
    }
    
    // 根据不同类型的目的地,获取对应的Navigator
    Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
            node.getNavigatorName());
    //添加导航图中设置的默认参数
    Bundle finalArgs = node.addInDefaultArgs(args);
    //发起导航
    NavDestination newDest = navigator.navigate(node, finalArgs,
            navOptions, navigatorExtras);
            
    if (newDest != null) {
        ...
        // 实际发生了导航,将跳转的页面添加到返回栈,如果其中包含 NavGraph 应添加到返回栈顶
        if (mBackStack.isEmpty() || mBackStack.getFirst().getDestination() != mGraph) {
            NavBackStackEntry entry = new NavBackStackEntry(mContext, mGraph, finalArgs,
                    mLifecycleOwner, mViewModel);
            mBackStack.addFirst(entry);
        }
        //最终将实际目的地添加到返回栈顶
        NavBackStackEntry newBackStackEntry = new NavBackStackEntry(mContext, newDest,
                newDest.addInDefaultArgs(finalArgs), mLifecycleOwner, mViewModel);
        mBackStack.add(newBackStackEntry);
    } else if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
        //singleTop 情况下 仅更新跳转数据
        launchSingleTop = true;
        NavBackStackEntry singleTopBackStackEntry = mBackStack.peekLast();
        if (singleTopBackStackEntry != null) {
            singleTopBackStackEntry.replaceArguments(finalArgs);
        }
    }
    //更新返回按钮的可点击状态
    updateOnBackPressedCallbackEnabled();
    if (popped || newDest != null || launchSingleTop) {
        //满足上述情况 发送 destinationChagned 回调
        dispatchOnDestinationChanged();
    }
}
复制代码

总结起来:

  1. 若配置了 popUpTo 属性,则先执行弹出动作(popBackStack 我们后面会讲到)。
  2. 根据 NavDestination 的 navigatorName 属性 从 navigatorProvider 中得到对应的 Navigator,通过上文我们已经知道几个内置名字: fragment、activity、dialog 等。
  3. 调用 navigator.navigate 方法执行具体的跳转动作。
  4. 如果3实际执行了跳转,那么会将 传入的 NavDestination 返回,此时将跳转过程中生成的 NavBackStackEntry 添加到返回栈中。
  5. 处理 singleTop 的情况。
  6. 更新返回按钮的可用性,默认情况下 如果返回栈中有实际的目的地,则 UI 层的返回按钮应该显示。
  7. 发送 destinationChagned 回调通知业务层。

至此,导航的整体流程结束,我们把其中最关键的节点列出做成如下流程图,帮助你从整体理解 Navigation 内部导航机制。

导航全流程.png

导航——返回

接下来,我们来看返回操作。navigation 提供两个方法处理导航返回: popBackStack 和 navigateUp,二者在大部分场景的行为都一致。

# NavController.java
public boolean navigateUp() {
   if (getDestinationCountOnBackStack() == 1) {
       ...
       return false;
   } else {
       return popBackStack();
   }
}

//获取实际目的地个数
private int getDestinationCountOnBackStack() {
   int count = 0;
   for (NavBackStackEntry entry : mBackStack) {
       if (!(entry.getDestination() instanceof NavGraph)) {
           count++;
       }
   }
   return count;
}
复制代码

可以看到,不同的是 navigateUp 在返回前会先检查 当前返回栈是否存在多余一个的 实际的目的地,也就是 NavDestination 而非虚拟的 NavGraph。

如果大于1,则直接执行 popBackStack;若等于 1,则判断 是否当前返回栈就是 其 NavGraph 的起始目的地,如果是 则说明该返回栈已经空了,什么都不做;反之,说明是通过 deeplink 跳转的过来的,此时会退出当前的 Activity,并且以之前 intent 跳转参数重新启动 Activity。

pop 过程比较简单,最终会走到 popBackStackInternal 方法。

# NavController.java
public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
    boolean popped = popBackStackInternal(destinationId, inclusive);
    return popped && dispatchOnDestinationChanged();
}
    
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
    if (mBackStack.isEmpty()) {
        return false;
    }
    ArrayList<Navigator<?>> popOperations = new ArrayList<>();
    //倒序遍历返回栈
    Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
    boolean foundDestination = false;
    while (iterator.hasNext()) {
        NavDestination destination = iterator.next().getDestination();
        Navigator<?> navigator = mNavigatorProvider.getNavigator(
                destination.getNavigatorName());
        //查找本次操作需要退出的页面
        if (inclusive || destination.getId() != destinationId) {
            popOperations.add(navigator);
        }
        if (destination.getId() == destinationId) {
            //返回栈中匹配到目的地,结束流程
            foundDestination = true;
            break;
        }
    }
    if (!foundDestination) {
        //没找到回退目标 什么都不做
        return false;
    }
    boolean popped = false;
    for (Navigator<?> navigator : popOperations) {
        //依次退出,不同 navigator 采取各自定义的退出动作
        if (navigator.popBackStack()) {
            //取出并移除返回栈
            NavBackStackEntry entry = mBackStack.removeLast();
            //清除返回栈viewmodel,更新 LifeState
            ...
            
            popped = true;
        }
        ...
    }
    //更新返回按钮状态
    updateOnBackPressedCallbackEnabled();
    return popped;
}
复制代码

相比原生的退出操作,Navigation 回退操作可以指定返回栈中的某个节点,一次性的弹出从这个节点开始(inclusive 参数控制是否包括自己这个节点)到栈顶的所有目的地。

另外,实际的回退动作还是交由 navigator 完成,我们以 FragmentNavigator 为例:

# FragmentNavigator.java
public boolean popBackStack() {
    if (mBackStack.isEmpty()) {
        return false;
    }
    ...
    //通过 fm 完成事务的回滚
    mFragmentManager.popBackStack(
            generateBackStackName(mBackStack.size(), mBackStack.peekLast()),
            FragmentManager.POP_BACK_STACK_INCLUSIVE);
    mBackStack.removeLast();
    return true;
}
复制代码

所以,Fragment 弹出栈最终还是通过 FM 回滚事务完成。

组织返回栈

在前文中讲到,NavGraph 虽然是虚拟的导航目的地,但是在跳转到一个 嵌套图的目的地(假设为 A)时,默认会将此 NavGraph 先添加到返回栈中,其次才是这个 A 本身。

下面举个例子,都在图里了。

组织返回栈.png

为什么我们清楚返回栈的情况?因为 返回栈本身的特殊性。Navigation 框架为我们开放了几个获取返回栈的 API。

# NavController.java
//获取整个返回栈
public Deque<NavBackStackEntry> getBackStack(){}

//根据目的地Id查找返回栈
public NavBackStackEntry getBackStackEntry(@IdRes int destinationId) {}

//获取当前栈
public NavBackStackEntry getCurrentBackStackEntry() {}

//获取上一个返回栈
public NavBackStackEntry getPreviousBackStackEntry() {}
复制代码

那么拿到 NavBackStackEntry 又能做什么呢?

返回栈 NavBackStackEntry

在 Navigation 上篇中我们讲到,可以将 VM 的范围限定为导航图级别 (by navGraphViewModels),这样做的好处是 本 Graph 中的 Fragment 都可以一个 VM 进行通信,而当这个 Graph 弹出返回栈后,这部分 VM 的资源就被释放了,这是相比于通过 Activity 范围的 VM 最大的好处。

我们直接看源码:

# NavBackStackEntry.java
public final class NavBackStackEntry implements
    LifecycleOwner,
    ViewModelStoreOwner, HasDefaultViewModelProviderFactory,
    SavedStateRegistryOwner { ... }
复制代码

通过 NavBackStackEntry 的四个实现类,我们可以快速了解 到底能做什么。

  • LifecycleOwner 说明它是一个可感知生命周期的组件,常见的可以用于设置 LiveData 的观察范围。
  • ViewModelStoreOwner 说明它自带 VM 存储器,可以控制 VM 的创建和缓存逻辑。每一个 NavBackStackEntry 对应一个固定的 owner。
  • HasDefaultViewModelProviderFactory 说明它管控了 VM 的创建过程。
  • SavedStateRegistryOwner 使用 SaveState 框架,结合 HasDefaultViewModelProviderFactory 完成自动的状态保存和恢复。

现在,我们来看 by navGraphViewModels 是如何工作的。

# NavGraphViewModelLazy.kt
inline fun <reified VM : ViewModel> Fragment.navGraphViewModels(
    @IdRes navGraphId: Int,
    noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null
): Lazy<VM> {
    //获取 navGraphId 对应的返回栈
    val backStackEntry by lazy {
        findNavController().getBackStackEntry(navGraphId)
    }
    //viewModelStore 就用该返回栈的
    val storeProducer: () -> ViewModelStore = {
        backStackEntry.viewModelStore
    }
    //默认使用自身提供的provider 创建 vm
    return createViewModelLazy(VM::class, storeProducer, {
        factoryProducer?.invoke() ?: backStackEntry.defaultViewModelProviderFactory
    })
}
复制代码

接下来是 backStackEntry.viewModelStore:

# NavBackStackEntry.java
public ViewModelStore getViewModelStore() {
    if (mNavControllerViewModel == null) {
        //抛异常
    }
    //mId 为每个返回栈在创建时生成的 UUID
    return mNavControllerViewModel.getViewModelStore(mId);
}

# NavControllerViewModel.java
ViewModelStore getViewModelStore(@NonNull UUID backStackEntryUUID) {
    //同一个 uuid会取到相同的 VM store
    ViewModelStore viewModelStore = mViewModelStores.get(backStackEntryUUID);
    if (viewModelStore == null) {
        viewModelStore = new ViewModelStore();
        mViewModelStores.put(backStackEntryUUID, viewModelStore);
    }
    return viewModelStore;
}
复制代码

由于 NavGraph 在返回栈中是先于实际目的地添加到返回栈中,弹出时又晚于实际目的地,所以这保证在该 Graph 被销毁前,作为其子目的地的媒介交互是可靠的。

返回事件的拦截

正常情况下,当 Activity 收到返回事件后,会直接 退出本页面。但由于 Navigation 是单 Activity 模式,默认应该先弹出返回栈,当返回栈没有目的地后才执行 Activity 的退出逻辑。

本节,我们来看一下导航框架是如何处理返回事件的,另外在 Fragment 中如何添加返回事件的拦截。

事实上 AndroidX 包中的 ComponentActivity 添加了对返回事件的拦截——OnBackPressedDispatcher。外部可以向 OnBackPressedDispatcher 添加回调,其内部维护一个 callback 列表,当收到返回事件时,会倒序依次回调返回列表,当某个回调消费了此返回事件,则停止遍历。若均没有消费,则会回调一个 fallback 方法。

# ComponentActivity.java
public class ComponentActivity extends androidx.core.app.ComponentActivity

    private final OnBackPressedDispatcher mOnBackPressedDispatcher =
        new OnBackPressedDispatcher(new Runnable() {
            @Override
            public void run() {
            //fallback 执行 finish
             ComponentActivity.super.onBackPressed();
            }
        });
            
    public void onBackPressed() {
        mOnBackPressedDispatcher.onBackPressed();
    }
    
    public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
        return mOnBackPressedDispatcher;
    }
}
复制代码

OnBackPressedDispatcher.png

另外,callback 是可感知生命周期的,也就是当注册回调的组件处 于unactive,则会自动移除回调。

以上,所以基于 ComponentActivity 开发就默认支持返回拦截了, Navigation 就在内部添加了一个默认的拦截器。

# NavHostFragment.onCreate()
//将Activity 使用的 Dispatcher 传入 Navigation
mNavController.setOnBackPressedDispatcher(requireActivity().getOnBackPressedDispatcher());

# NavController.java
void setOnBackPressedDispatcher(OnBackPressedDispatcher dispatcher) {
    ...
    dispatcher.addCallback(mLifecycleOwner, mOnBackPressedCallback);
    ...
}

// enable属性控制该拦截器是否生效
private boolean mEnableOnBackPressedCallback = true;

private final OnBackPressedCallback mOnBackPressedCallback =
        new OnBackPressedCallback(false) {
    @Override
    public void handleOnBackPressed() {
        //拦截返回的处理就是弹出返回栈
        popBackStack();
    }
};

复制代码

前文我们讲到每次跳转和返回最后都会执行 updateOnBackPressedCallbackEnabled,事实上也是在更新这个拦截器的可用性。

# NavController.java
private void updateOnBackPressedCallbackEnabled() {
    mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
            && getDestinationCountOnBackStack() > 1);
}
复制代码

上层可以通过 OnBackPressedDispatcher 提供的 addCallback 接口完成返回的拦截,如果不需要拦截了,将拦截器的 enable 属性设置为 false。

//添加 LifeCycle 生命周期感知
requireActivity().onBackPressedDispatcher.addCallback(this@MainFragment) {
    //添加自定义拦截
}
复制代码

Navigation 的几个 Tips

跳转返回后 view 被重建产生的影响

对于新手开发来说,碰到最多的问题就是:为什么跳转返回后原页面的“状态”怎么乱了?最终查到问题是因为,Fragment 中 VM 持有的 LiveData 再次收到了 observe 的回调,而再次收到回调是因为 fragment 生命周期由 unactive 变为了 active,由于 LiveData 本身的回调是 粘性的,所以最终再次收到了回调。

所以在业务层需小心处理粘性回调对业务影响,因为并不是所有回调都是幂等的。

另外,视图的状态保存通常你是不需要关心的,比如 EditText 已输入的内容或者列表的滚动位置,因为 FragmentManager 在 View 被移除的时候会调用 view 树上所有 view 的 onSaveInstanceState,当然前提是你的 View 设置了 ID。

同时在 Fragment 再次回到前台时执行 onRestoreInstanceState

为 start destination 设置入参

默认情况下 Navigation 不支持为 start destination 设置入参,如果需要传参需使用代码完成 graph 的初始化。去掉 xml 中设置的graph,转用代码设置 graph,并指定入参。

mNavController.setGraph(graphId, startDestinationArgs);
复制代码

对于 "onFragmentResult" 的支持

事实上,在 AndroidX Fragment 1.3.0-alpha04 版本开始就支持了 onFragmentResult API, 参考官方文档: 使用 Fragment Result API 获取结果

NavBackStackEntry 也提供了一种通过 SaveStateHandle 的方式实现通信。

//第一个页面添加监听
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("result")?.observe(
    viewLifecycleOwner) { result ->
    //处理返回结果
}

//第二个页面设置返回数据
findNavController().previousBackStackEntry?.savedStateHandle?.set("result", result)
复制代码

SaveStateHandle 保证在应用在后台被清除进程了 依然可以恢复数据。

这里附上各种情况下 状态恢复的支持情况:

操作变量视图状态SavedStateNonConfig
添加到返回堆栈x
配置更改x
进程终止/重新创建x
被移除,不添加到返回堆栈xxxx
宿主已完成xxxx

详见: 保存与 Fragment 相关的状态

多返回栈的支持

由于 Navigation 的默认实现是直接用新的目的地 replace 旧的目的地,而不对嵌套图做任何额外的恢复工作,这导致一些简单的场景用 Navigation 实现会异常复杂。

比如 底部导航栏 + Fragment 切换,假设有两个Tab 分别为 首页 和 个人,首页 Tab 支持内部跳转到搜索页面,当用户处于搜索页面时 切换 Tab 到 个人后,再次切回首页,如果用 Navigation 实现 将会回到首页,而不是首页的二级搜索页。

这本质上是 Navigation 不支持 多个返回栈的状态保存。

也就是首页和个人都是嵌套图且在导航图中的为兄弟节点,那么在这两个嵌套图来回跳转时应当保持各自的返回栈。

Navigation 从 2.4.0 版本开始支持多返回栈,目前仍为 alpha 版本,可以尝试在测试阶段使用。

关于我

  • 掘金
  • 公众号 wanderingTech
文章分类
Android