Android Jetpack
已经出来很久了,目前在自己的 开源项目 中体验了一把,不得不说很舒服,除了有一些坑之外,这次主要讲解下 Jetpack
中的 Navigation
,Navigation
主要用来管理 Fragment
,方便实现单个 Activity
及 N 多个 Fragment
的 App
,Navigation
的使用网上一搜一大把,这里主要通过源码,分析下 Navigation
是如何实现 Fragment
的管理
从布局入手
Navigation
通过指定布局中的 fragment
即可实现,即
frameLabelStart--frameLabelEnd
所以我们就从 NavHostFragment
这个类开始入手
NavHostFragment && NavHost
public class NavHostFragment extends Fragment implements NavHost {}
该 Fragment
实现了 NavHost
接口,这边先跳开下,看下这个接口需要实现的方法
/**
* A host is a single context or container for navigation via a {@link NavController}.
*/
public interface NavHost {
/**
* Returns the {@link NavController navigation controller} for this navigation host.
*
* @return this host's navigation controller
*/
@NonNull
NavController getNavController();
}
看下官方给该接口的定位,「是个 NavController
的宿主」,NavController
是啥,我们后面再来看,回到 NavHostFragment
,首先看下用于 Fragment
初始化常用的几个方法 onInflate
,onAttach
,onViewCreated
,onCreateView
以及 onCreate
onInflate
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,
@Nullable Bundle savedInstanceState) {
// 省略一些非关键代码...
// 映射布局的 navGraph 属性,并赋值给 mGraphId,该值用于指定导航图
final int graphId = navHost.getResourceId(R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
// 省略一些非关键代码...
// 映射布局的 defaultNavHost 并赋值给 mDefaultNavHost,该值用于设置是否将返回键控制权给 fragment
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
if (defaultHost) {
mDefaultNavHost = true;
}
}
onAttach
@CallSuper
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
// 如果设置获取返回键控制权的属性为 true,通过 setPrimaryNavigationFragment 方法进行设置
// 否则,控制权还是在 activity
if (mDefaultNavHost) {
requireFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
}
onViewCreated
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 该方法通过设置 view 的 tag 属性为 controller,后期获取 controller 可能会使用,下同
Navigation.setViewNavController(view, mNavController);
if (view.getParent() != null) {
View rootView = (View) view.getParent();
if (rootView.getId() == getId()) {
Navigation.setViewNavController(rootView, mNavController);
}
}
}
onCreateView
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// FragmentContainerView 实际是一个 FrameLayout,在该生命周期中,将 fragment 的 id 设置给父布局
FragmentContainerView containerView = new FragmentContainerView(inflater.getContext());
containerView.setId(getId());
return containerView;
}
onCreate
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
// 初始化 NavController 的一些属性,并将 controller 设置给宿主
// 包括关联 lifeCycler,返回键的监听属性等
mNavController = new NavHostController(context);
// ... 省略一些属性设置代码
// 在 onCreateNavController 方法中,给 controller 中的 NavigatorProvider 添加了
// DialogFragmentNavigator 和 FragmentNavigator,这两个类具体实现了什么,先留点悬念,稍后解读
onCreateNavController(mNavController);
// 获取 store 的状态,并判断是否要获取返回键控制
Bundle navState = null;
if (savedInstanceState != null) {
navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
mDefaultNavHost = true;
requireFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit();
}
}
// 将保存的状态设置回去
if (navState != null) {
mNavController.restoreState(navState);
}
// 将映射的 navigation 布局设置给 controller
if (mGraphId != 0) {
// Set from onInflate()
mNavController.setGraph(mGraphId);
} else {
// See if it was set by NavHostFragment.create()
final Bundle args = getArguments();
final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
final Bundle startDestinationArgs = args != null
? args.getBundle(KEY_START_DESTINATION_ARGS)
: null;
if (graphId != 0) {
mNavController.setGraph(graphId, startDestinationArgs);
}
}
}
通过上述的几个方法,将 NavController
,defaultNavHost
,NavGraph
的值初始化完成,在 NavHostFragment
中还有个非常重要的方法 findNavController
,通过该方法,可以获取到 Fragment
的管理者 NavController
findNavController
@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
Fragment findFragment = fragment;
while (findFragment != null) {
// 如果当前传入的 fragment 就是 NavHostFragment 则直接返回 onCreate 中初始化的 mNavController
if (findFragment instanceof NavHostFragment) {
return ((NavHostFragment) findFragment).getNavController();
}
// 如果不是则通过 onAttach / onCreate 方法中通过 setPrimaryNavigationFragment 方法
// 设置的 fragment 并返回 mNavController
Fragment primaryNavFragment = findFragment.requireFragmentManager()
.getPrimaryNavigationFragment();
if (primaryNavFragment instanceof NavHostFragment) {
return ((NavHostFragment) primaryNavFragment).getNavController();
}
// 如果上述都不成立,则获取父级的 Fragment,继续循环去判断获取
findFragment = findFragment.getParentFragment();
}
// Try looking for one associated with the view instead, if applicable
View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}
throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}
所以,当我们封装 Fragment
基类的时候,即可通过该方法,为所有的 Fragment
寻找其对应的 NavController
在介绍 NavHostFragment
的时候,有个类 NavController
也出现了多次,该 Fragment
就是其宿主,接着就看下 Controller
里面做了什么操作
NavController
NavController
作为整个 App
的 Fragment
管理者,有几个比较重要的方法,包括 SetGraph
设置「导航图」,navigate
跳转 fragment
界面,navigateUp
返回回退栈上个界面,getNavInflater
用于映射 navigation.xml
文件
setGraph
setGraph
重载的方法比较多,但最终会调用 onGraphCreated
方法
private void onGraphCreated(@Nullable Bundle startDestinationArgs) {
// 获取之前保存的状态,并设置状态至 Navigator,Navgator 通过 name 存在 NavigatorProvider 中
// 在 NavigatorProvider 中有个 HashMap 用来存储 Navigator
if (mNavigatorStateToRestore != null) {
ArrayList<String> navigatorNames = mNavigatorStateToRestore.getStringArrayList(
KEY_NAVIGATOR_STATE_NAMES);
if (navigatorNames != null) {
for (String name : navigatorNames) {
Navigator<?> navigator = mNavigatorProvider.getNavigator(name);
Bundle bundle = mNavigatorStateToRestore.getBundle(name);
if (bundle != null) {
navigator.onRestoreState(bundle);
}
}
}
}
if (mBackStackToRestore != null) {
for (Parcelable parcelable : mBackStackToRestore) {
// ... 省略一些获取属性的代码
// ... 设置属性并压入回退栈
NavBackStackEntry entry = new NavBackStackEntry(mContext, node, args,
mLifecycleOwner, mViewModel,
state.getUUID(), state.getSavedState());
mBackStack.add(entry);
}
// 更新当前是否可以获取系统返回按钮的控制权
updateOnBackPressedCallbackEnabled();
mBackStackToRestore = null;
}
// 当设置完「导航图」后,判断是否有 deepLink 属性,如果没有则显示第一个界面
// deepLink 用于设置 url,可直接跳转指定的界面
// 例如,当收到通知后需要跳转指定界面,则可以通过 deepLink 实现
if (mGraph != null && mBackStack.isEmpty()) {
boolean deepLinked = !mDeepLinkHandled && mActivity != null
&& handleDeepLink(mActivity.getIntent());
if (!deepLinked) {
// Navigate to the first destination in the graph
// if we haven't deep linked to a destination
navigate(mGraph, startDestinationArgs, null, null);
}
}
}
navigate
navigate
用于跳转界面,重载的方法也较多,最终调用的内部私有方法 navigate
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
boolean popped = false;
// navOptions 用于设置跳转的动画,pop 时候对应的界面等,具体可以查看 NavOptions 类
if (navOptions != null) {
if (navOptions.getPopUpTo() != -1) {
popped = popBackStackInternal(navOptions.getPopUpTo(),
navOptions.isPopUpToInclusive());
}
}
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(
node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
// 实际通过 Navigator.navigate 进行跳转
// Navigator 是个抽象类,具体实现类有 ActivityNavigator,FragmentNavigator,
// DialogFragmentNavigator,NavGraphNavigator,NoOpNavigator等,且在类头部使用了 Name 注解,
// 通过 Name 注解,能够在 NavigatorProvider 注册相应的 Navigator
// 在 navigation.xml 布局中,通过 Name 对应的值,进行注册即可,
// 例如注册 fragment 则直接使用 <fragment></fragment> 标签,
// 同时还有 <activity></activity>,<dialog></dialog>,<navigation></navigation> 等标签
NavDestination newDest = navigator.navigate(node, finalArgs,
navOptions, navigatorExtras);
if (newDest != null) {
if (!(newDest instanceof FloatingWindow)) {
// 如果跳转的界面不是 FloatingWindow 则持续通过 popBackStackInternal 出栈,一直到满足条件
while (!mBackStack.isEmpty()
&& mBackStack.peekLast().getDestination() instanceof FloatingWindow
&& popBackStackInternal(
mBackStack.peekLast().getDestination().getId(), true)) {
// Keep popping
}
}
// ... 省略入栈部分,当跳转完成后,则通知监听
if (popped || newDest != null) {
dispatchOnDestinationChanged();
}
}
navigateUp
navigateUp
用于回退上个界面,当调用该方法时,会通过回退栈中的数量进行不同处理,如果数量为 1 则会直接 finish
对应的 activity
,否则调用 popBackStack
方法,而 popBackStack
最终会调用 popBackStackInternal
方法,该方法返回一个 Boolean
值,用于判断是否出栈成功
boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) {
// ...
// 列表用于存储需要出栈的 Navigator
ArrayList<Navigator<?>> popOperations = new ArrayList<>();
Iterator<NavBackStackEntry> iterator = mBackStack.descendingIterator();
boolean foundDestination = false;
// 遍历回退栈的,并将符合出栈条件的 Navigator 放入列表
// 如果已经找到了需要的 destination 则打断循环
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;
}
}
//...对需要出栈的进行出栈处理
return popped;
}
getNavInflater
getNavInflater
通过将 mNavigatorProvider
传给 NavInflater
,前面提到过,NavigatorProvider
是用来保存一系列的 Navigator
,那么当传入到 NavInflater
中后,该类会对包含的 Navigator
进行解析成一个个 Destination
,用于导航跳转,具体如何解析的有兴趣的朋友可以自己看
在上面的 navigate
方法中,提到了实际跳转是通过 Navigator #navigate
进行跳转的,但是 Navigator
是个抽象类,具体的实现由子类完成,因为更多的会使用 fragment
,所以我们只看下 FragmentNavigator
类下的 navigate
方法
FragmentNavigator
navigate
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
// ...
// 通过 destination 的 className 寻找相应的 Fragment,并设置一些传递的参数
String className = destination.getClassName();
if (className.charAt(0) == '.') {
className = mContext.getPackageName() + className;
}
final Fragment frag = instantiateFragment(mContext, mFragmentManager,
className, args);
frag.setArguments(args);
final FragmentTransaction ft = mFragmentManager.beginTransaction();
// ...设置一些动画等属性
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
final @IdRes int destId = destination.getId();
final boolean initialNavigation = mBackStack.isEmpty();
// TODO Build first class singleTop behavior for fragments
final boolean isSingleTopReplacement = navOptions != null && !initialNavigation
&& navOptions.shouldLaunchSingleTop()
&& mBackStack.peekLast() == destId;
boolean isAdded;
// 根据是否是 singleTop,做不同的入栈处理
if (initialNavigation) {
isAdded = true;
} else if (isSingleTopReplacement) {
// Single Top means we only want one instance on the back stack
if (mBackStack.size() > 1) {
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;
}
// ...设置一些共享元素
ft.setReorderingAllowed(true);
ft.commit();
// The commit succeeded, update our view of the world
if (isAdded) {
mBackStack.add(destId);
return destination;
} else {
return null;
}
}
NavAction && NavDestination
除了上述的几个类以外,Navigation
还有比较重要的就是 NavAction
和 NavDestination
,NavAction
中指定了跳转的 DestinationId
,额外的携带参数等,可以简单的看成一个实体类,NavDestination
中则包含了各种 NavAction
,DeepLink
等多种属性,构成了「导航图」上的一个个点。
解决重新创建 Fragment
的坑
Navigation
目前比较大的一个坑就是存在 Fragment
在重新回到界面上的时候会重新创建,既然是坑,那就得解决啊,这边我们借助 ViewModel + LiveData
来完成,封装一个基类
abstract class BaseFragment<VB : ViewDataBinding> : Fragment() {
protected var mBinding: VB? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
retainInstance = true
// 保证只会创建一次 view,然后通过 ViewModel + LiveData 对 view 显示内容进行控制
if (mBinding == null) {
mBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
actionsOnViewInflate()
}
return mBinding?.root
}
// 该方法完整走完一个生命周期只会走一次,可用于该页面进入时网络请求
open fun actionsOnViewInflate() {}
abstract fun getLayoutId(): Int
}
但是按照这么封装,在使用 ViewPager + Fragment
的时候会出现重复添加的问题,再做下修改,将添加的先从父布局移除,再添加,就可以完美解决 Navigation
留下的坑
abstract class BaseFragment<VB : ViewDataBinding> : Fragment() {
protected var mBinding: VB? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
retainInstance = true
if (mBinding == null) {
mBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
actionsOnViewInflate()
}
// 解决 ViewPager + Fragment 情况下重复添加的问题
return if (mBinding != null) {
mBinding!!.root.apply { (parent as? ViewGroup)?.removeView(this) }
} else super.onCreateView(inflater, container, savedInstanceState)
}
}
一张图总结
看了那么多源码,最后用一张比较形象的图来结束吧