前言
在以往的Fragment使用中,我们都是使用Fragment的事务进行添加,删除,替换等操作,为了快速开发,我们也会自行封装一个FragmentController。在去年,Google推出了Navigation库,目标是更优雅的管理Fragment。
正文
首先我们回顾一下Fragment的事务:
fragmentManager.beginTransaction().add(xxx).commit();
如果是常见的多Tab切换Fragment,我们会在XML中使用FrameLayout作为Fragment的容器,然后创建Fragment实例,根据不同情况放入FrameLayout中:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
假设我们要阅读这份代码,坦白的说,你从这个xml可以得到的信息非常的少,你只能猜测这个页面可能是使用了Fragment仅此而已,然后再去找Java或Kotlin文件,具体查看FrameLayout都使用了哪些功能逻辑。
Navigation
1.什么是Navigation
Navigation是一个可简化的Android导航的库和插件,换句话说,Navigation是用来管理Fragment的切换的,并且是通过可视化的方式来进行管理的。
2.Navigation的优缺点
优点
- 处理Fragment的切换
- 默认情况下正确处理Fragment的前进和后退
- 为过渡和动画提供标准化的资源
- 可以绑定Toolbar/BottomNavigationView/ActionBar等
- 数据传递时提供类型安全性(使用SafeArgs)
- ViewModel支持
缺点
- fragment切换后底层会调用replace方法导致会被不断销毁,无法保存上一次的状态
3.Navigation的使用
Navigation的使用相对来说比较简答,分为以下几步:
(1)引入依赖
(2)创建多个要调配的Fragment
(3)在res下面创建navigation文件夹,并创建navigation文件
(4)在主Activity里面的XML文件里面引入指定的Fragment 基本上大体步骤就那么几步,现在我们就一个一个来看,完成刚才的多Tab切换逻辑:
MainActivity的xml文件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- fragment的集合 -->
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
nav_graph文件:
<?xml version="1.0" encoding="utf-8"?>
<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/mainFragment"> <!-- 开始的fragment -->
<fragment
android:id="@+id/mainFragment"
android:name="com.lzp.navigation.fragment.MainFragment"
android:label="main"
tools:layout="@layout/fragment_main" />
<fragment
android:id="@+id/secondFragment"
android:name="com.lzp.navigation.fragment.SecondFragment"
android:label="second"
tools:layout="@layout/fragment_sec" />
</navigation>
从代码量上来看,确实是增加了,但是对应的xml中可以查看的信息增加了很多,从Activity的XML中我们把Fragment的使用区域封装成一个Fragment,而这个Fragment绑定了一个@navigation/nav_graph文件,在nav_graph中描述了我们将会使用到哪些Fragment。把Fragment的维护移动到XML中,尽可能简化Fragment的使用复杂度,提高代码的可阅读性和维护性。你可以把Navigation的使用看成是一个高级的Include,只不过他的功能更加丰富和强大。
添加Gradle依赖
dependencies {
def nav_version = "2.1.0"
// Java
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
// Kotlin
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}
Google提供了Java和Kotlin两个版本。想要使用Navigation,必须要支持androidX
使用NavHostFragment
<!-- 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:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
把FrameLayout容器替换成NavHostFragment,app:navGraph="@navigation/nav_graph" 是绑定对应的布局文件。@navigation只有在android studio 3.3以上版本才支持。
创建navGraph
在res文件加下创建navigation文件夹,在该文件夹下创建你需要的xml.
我们将会使用两个Fragment,分别为MainFragment和SecondFragment,要为他们设置好id,因为Fragment的切换需要使用id。app:startDestination="@id/mainFragment" 必须设置,指定默认添加的Fragment的id,如果不设置会直接崩溃。
切换Fragment
从MainFragment切换到SecondFragment:
val navHostController = Navigation.findNavController(activity, R.id.nav_host_fragment)
// 跳转到secondFragment
navHostController.navigate(R.id.secondFragment)
// 返回上一个Fragment
navHostController.navigateUp()
Navigation的更多理解
Navigation的使用就是这么简单,Fragment的控制几乎都在NavController中。
先简单看一下Navigation框架大致的实现原理。
在容器Activity的布局文件中,我们使用一个<fragment>标签,并且为标签显示的指定了一个android:name属性,里面配置的是一个Fragment的全路径,官方提供的是androidx.navigation.fragment.NavHostFragment,我们都知道,Activity加载布局的时候会根据配置的全路径通过反射获取到Fragment对象,然后attach到该Activity,最终完成Fragment的加载。想要了解Navigation框架,从NavHostFragment入手再合适不过。
public class NavHostFragment extends Fragment implements NavHost {
...
}
public interface NavHost {
@NonNull
NavController getNavController();
}
NavHostFragment就是一个Fragment的子类实现了一个简单的接口,能够对外提供获取NavController的方法,该方法的返回值就是NavHostFragment的一个属性mNavController。
mNavController属性的初始化是在onCreate生命周期中完成的。
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = requireContext();
mNavController = new NavHostController(context);
mNavController.setLifecycleOwner(this);
//略...调用一些mNavController方法
onCreateNavController(mNavController); //这个方法比较重要,下面会提及
//设置导航图ID
if (mGraphId != 0) {
mNavController.setGraph(mGraphId);
} else {
//设置一个空导航
}
}
mGraphId就是在fragment标签中配置的navGraph属性是在onInflate方法中获取的:
@CallSuper
@Override
public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs,@Nullable Bundle savedInstanceState){
super.onInflate(context, attrs, savedInstanceState);
final TypedArray navHost = context.obtainStyledAttributes(attrs,androidx.navigation.R.styleable.NavHost);
//通过自定义属性获取navigation导航图
final int graphId = navHost.getResourceId(androidx.navigation.R.styleable.NavHost_navGraph, 0);
if (graphId != 0) {
mGraphId = graphId;
}
...
}
其实NavHostFragment才是容器Activity加载的第一个Fragment,在mNavController.setGraph方法调用之后,会经过一些列的方法调用,最终替换为在navigation资源文件中配置的startDestination属性中的Fragment。
NavController虽然看起来比较多,但它的功能还是比明确的,就是对外提供设置NavGraph、跳转方法navigate、返回事件控制以及监听Destination的变化。但真正执行视图跳转的逻辑并不是NavController执行的,而是通过mNavigatorProvider分发到了不同的Navigator中,然后执行真正的跳转逻辑:
//NavController中navigate最终的重载
private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//...
//根据跳转类型的不同,分发到不同的navigator中执行跳转逻辑
Navigator<NavDestination> navigator = mNavigatorProvider.getNavigator(node.getNavigatorName());
Bundle finalArgs = node.addInDefaultArgs(args);
//调用navigator里的navigate方法
NavDestination newDest = navigator.navigate(node, finalArgs,navOptions, navigatorExtras);
//...更新mBackStack栈
}
抽象类Navigator一共有5个子类:
@Navigator.Name("activity")
public class ActivityNavigator extends Navigator<ActivityNavigator.Destination> {
//... 控制Activity的跳转
}
@Navigator.Name("dialog")
public final class DialogFragmentNavigator extends Navigator<DialogFragmentNavigator.Destination> {
//...控制DialogFragment的跳转
}
@Navigator.Name("fragment")
public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
//...控制Fragment的跳转
}
@Navigator.Name("navigation")
public class NavGraphNavigator extends Navigator<NavGraph> {
//...控制变更NavGraph
}
@Navigator.Name("NoOp")
public class NoOpNavigator extends Navigator<NavDestination> {
//...忽略不计...
}
NavigatorProvider类负责管理以上五种Navigator,管理的方式非常简单,就是用一个名为mNavigators的HashMap<String,Navigator>把通过addNavigator方法添加的Navigator缓存起来,其中key就是@Navigator.Name("xxx")注解里面给定的xxx,在getNavigator时从缓存中取出来给调用方。
在我们使用NavHostFragment的时候,框架会为我添加前四种Navigator,分别是在上文提到过的NavHostFragment的onCreate方法中的调用的onCreateNavController(mNavController)方法:
@CallSuper
protected void onCreateNavController(@NonNull NavController navController) {
//添加DialogFragmentNavigator
navController.getNavigatorProvider().addNavigator(
new DialogFragmentNavigator(requireContext(), getChildFragmentManager()));
//添加FragmentNavigator
navController.getNavigatorProvider().addNavigator(createFragmentNavigator());
}
和NavController的构造方法里:
public NavController(@NonNull Context context) {
mContext = context;
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
mActivity = (Activity) context;
break;
}
context = ((ContextWrapper) context).getBaseContext();
}
//这里
mNavigatorProvider.addNavigator(new NavGraphNavigator(mNavigatorProvider));
mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
}
以上就是Navigation框架的大体逻辑,总结一下就是:
NavHostFragment作为容器Activity第一个加载的Fragment,维护了一个NavController的实例,并在NavigatorProvider中添加了4种Navigator用来执行不同的视图跳转逻辑,并在onCreate方法的最后,通过NavController.setGraph方法设置了在fragment标签中配置的nvGraph的id,把NavHostFragment重定向到了navigation.xml里配置的startDestination。NavController的跳转逻辑也通过跳转类型的,通过内部维护的NavigatorProvider分发到了不同的Navigator进行跳转。
情况就很明了了,我们在切换Fragment中调用的跳转方法:
最终会经过一系列的处理分发到FragmentNavigator的navigate方法中去
@Nullable
@Override
public NavDestination navigate(@NonNull Destination destination, @Nullable Bundle args,
@Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) {
//略...
final FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.replace(mContainerId, frag);
ft.setPrimaryNavigationFragment(frag);
//略...
ft.setReorderingAllowed(true);
ft.commit();
//略...
}
看到这里终于恍然大悟,原来Navigation框架还是基于FragmentTransaction的封装!因为在打开新的Fragment的时候,老Fragment直接被replace掉了。
|