Fragment管理库:Navigation


前言

在以往的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掉了。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值