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
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
的坑
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)
}
}
一张图总结
看了那么多源码,最后用一张比较形象的图来结束吧
