1. Jetpack源码解析---看完你就知道Navigation是什么了?

Posted Hankkin_Coding

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1. Jetpack源码解析---看完你就知道Navigation是什么了?相关的知识,希望对你有一定的参考价值。

1. 背景

之前已经翻译过了Google官方的CodeLabs上面的教程,教程很详细,代码在Github上也可以找到,本篇文章旨在自己的APP上使用效果及演示Demo,来具体的使用Navigation。并且对其进行源码解析。

基本相关介绍可以查看我之前翻译的文章,基本就是google翻译了一个大概。

一、Android Jetpack_Note_CodeLabs一Navigation

2. 基本使用

虽然在之前的文章中已经很详细的介绍了Navigation,但是这里也简单的叙述一下我在项目中的具体使用:

2.1 Navigation+DrawerLayout+ToolBar

我们可以通过使用Navigation 配合DrawerLayout侧边栏和Toolbar标题来进行工作,不再需要我们去定义点击事件,也不需要我们去管理Fragment做切换,只需要我们做相关的配置和极少量的代码就可以了。

2.1.1 DrawerLayout

侧边栏的用法和我们之前的使用一样,配置好我们NavigationView里面的_headerLayout__menu_即可;

**注意:**这里面的menu有一点和我们之前的不一样,item的id必须要和navigation里面的fragment的id相同,否则点击事件不生效,这里先提一下,下面会详细介绍。

2.1.2 ToolBar和NavHostFragment

DrawerLayout配置好之后,我们再来配置标题栏,之前我们的用法都是在中间加一个存放Fragment的容器,有可能是FrameLayoutViewPager等,这里面我们需要配置一个Fragment,这个Fragmentnameandroidx.navigation.fragment.NavHostFragment,这是一个添加到布局中的特殊部件,NavHostFragment通过navGraphnavigation导航编辑器进行关联。具体代码如下:

<androidx.drawerlayout.widget.DrawerLayout
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@+id/drawer_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            tools:openDrawer="start">
        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">
            <com.google.android.material.appbar.AppBarLayout
                    android:layout_height="wrap_content"
                    android:layout_width="match_parent"
                    android:theme="@style/AppTheme.AppBarOverlay">

                <androidx.appcompat.widget.Toolbar
                        android:id="@+id/toolbar"
                        android:layout_width="match_parent"
                        android:layout_height="?attr/actionBarSize"
                        android:background="?attr/colorPrimary"
                        android:theme="@style/AppTheme.PopupOverlay"
                />

            </com.google.android.material.appbar.AppBarLayout>

            <fragment
                    android:id="@+id/fragment_home"
                    android:name="androidx.navigation.fragment.NavHostFragment"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    app:defaultNavHost="true"
                    app:navGraph="@navigation/navigation_main"/>
        </LinearLayout>

        <com.google.android.material.navigation.NavigationView
                app:itemIconTint="@color/nav_item_txt"
                app:itemTextColor="@color/nav_item_txt"
                android:id="@+id/nav_view"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_gravity="start"
                android:fitsSystemWindows="true"
                app:headerLayout="@layout/nav_header_main"
                app:menu="@menu/activity_main_drawer"/>

    </androidx.drawerlayout.widget.DrawerLayout>

我们可以看到NavHostFragment中有两个属性比较特殊:app:defaultNavHostapp:navGraph="@navigation/navigation_main",前者就是是否是默认的其实页面,后者就是我们要设计的Navigation布局文件.

2.1.3 navigation_main.xml

Android Studio3.2版本以上里面内嵌了Navigation的设计面板工具,我们可以在res文件夹下面的navigation文件里面对我们的fragment/Activity进行设计。

打开Desgin面板,进入设计模式,在里面我们可以新建我们的目标页面。如果你还没创建过一个**Destination,**你可以点击create a destination创建一个Fragmengt/Activity。当然如果你之前已经创建好了的话,在这里你可以直接选择:

选择完一个Destination之后,在面板中就可以看到了,具体的action、arguments就不介绍了,详细的可以看之前的文章。

打开Text模式的xml我们可以看到我们选择的Fragmengt配置信息,当然你也可以不通过面板设计,也可以直接在xml里进行代码编写。
startDestination是APP默认启动的页面,这里面必须要指定,否则会报错crash。这里我的代码所指默认页面是HomeFragment,如下:

<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/navigation_main"
            app:startDestination="@+id/homeFragment"
            tools:ignore="UnusedNavigation">
  
 <fragment android:id="@+id/homeFragment"
              android:name="com.hankkin.jetpack_note.ui.home.HomeFragment"
              android:label="@string/menu_home">
        <action android:id="@+id/action_navigationFragment_to_webFragment"
                app:destination="@id/webFragment"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"/>
    </fragment>

    <fragment android:id="@+id/codeFragment"
              android:name="com.hankkin.jetpack_note.ui.CodeFragment"
              android:label="@string/menu_code"/>

我们可以看到上面的布局代码 默认的起始页面是homeFragment,下面还有一个codeFragment,其实这两个fragment也就是对应着在menu中的两个菜单,同时也对应我们侧边栏中的一个首页和一个代码页,

<item
                android:id="@+id/homeFragment"
                android:icon="@drawable/ic_menu_home"
                android:title="@string/menu_home"/>
        <item
                android:id="@+id/codeFragment"
                android:icon="@drawable/ic_menu_code"
                android:title="@string/menu_code"/>

还记得上面说的id要相同吗?就是上面item的id要和navigation_main.xml中fragment的id相同,否则点击菜单不会切换fragment的。

配置完上面这些信息之后,怎么将他们绑定起来使用呢?

2.1.4 NavController

先看下代码:

		navController = Navigation.findNavController(this, R.id.fragment_home)
        appBarConfiguration = AppBarConfiguration(setOf(R.id.homeFragment, R.id.codeFragment), drawerLayout)
        // Set up ActionBar
        setSupportActionBar(mDataBinding.toolbar)
        setupActionBarWithNavController(navController, appBarConfiguration)
        // Set up navigation menu
        mDataBinding.navView.setupWithNavController(navController)
  • 我们通过findNavController传入之前定义好的装载fragment的容器id(也就是之前定义的NavHostFragment)找到了Navigation对应的navController;
  • 通过配置一个AppBarConfiguration,AppBarConfiguration 里传入了一个id的set集合和drawerlayout,id的集合就是我们在**navigation_main.xml **定义的fragment id
  • 最后通过设置setupActionBarWithNavController、setupWithNavController进行关联绑定

到此,我们的基本配置就结束了,可以看到我们drawerlayout中的首页和代码按钮点击会切换对应的fragment,同时toolbar的汉堡按钮和返回按钮也会自动切换;当然Navigation还可以配合BottomNavigationView使用。

2.2 BottomNavigationView使用

2.2.1 配置文件

和上面的步骤类似:也是配置好 navigation.xml布局以及 BottomNavigationView所对应的menu菜单文件

2.2.2 setupWithNavController

当然BottomNavigationView也提供了扩展方法setupWithNavController去绑定菜单和fragment,这里使用很简单就不具体介绍了。详情可见BottomNavSampleActivity

2.3 Action跳转及传餐

2.3.1 Action跳转

先看一下navigation的Desgin模式:

可能你会注意到这些线是什么?没错这就是一个一个的Action,当你手动将两个Fragment进行连线后,在xml布局里面会对应生成一个标签,例如:

<action android:id="@+id/action_dashBoardSampleFragment_to_notificationSampleFragment"
                app:destination="@id/notificationSampleFragment"/>


它会自动创建好id,id有可能比较长,但是确很清楚,从xtoy的模式,当然如果你不喜欢可以自己改,destination则是我们要跳转到的目标接界面。

action设置好了之后,我们可以执行下面代码进行跳转:

findNavController().navigate(R.id.action_homeSampleFragment_to_dashBoardSampleFragment_action)

2.3.2 NavOptions切换动画

当然fragment之间的切换是支持动画的,NavOptions是一个动画管理类,我们可以设置进入和回退的动画,设置的方式有两种:

  1. 直接在标签中设置动画
<action android:id="@+id/action_homeSampleFragment_to_dashBoardSampleFragment_action"
                app:destination="@id/dashBoardSampleFragment"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"/>
  1. 通过NavOptions设置动画
val options = navOptions 
            anim 
                enter = R.anim.slide_in_right
                exit = R.anim.slide_out_left
                popEnter = R.anim.slide_in_left
                popExit = R.anim.slide_out_right
            
        
        view.findViewById<Button>(R.id.navigate_destination_button)?.setOnClickListener 
            findNavController().navigate(R.id.flow_step_one_dest, null, options)
        

2.3.3 参数传递

fragment之间的切换参数传递的方法也很简单,之前我们可能要通过宿主Activity或者接口等方法,总之挺麻烦的,下面我们看看通过Navigation控制的Fragment之间怎么传递?

我们可以在naviagtion布局中使用标签,

  • name是我们传参的key
  • argType是参数类型
  • defaultValue默认值
  • nullable 是否可空
<argument
         android:name="deep_args"
         app:argType=""
         android:defaultValue=""
         app:nullable=""/>

**注意:**当然type类型也支持我们自定的实体类,但是需要你填写类的全路径,同时你要保证实体类实现了序列化

我们可以通过把参数传递封装到Bundle中,然后再执行navigate()方法时传递过去,例如:

val args = Bundle()
args.putString("link","1")
args.putString("title","1")
it.findNavController().navigate(R.id.webFragment, args)

当然你在接受是也可以通过getArguments().getString(xxxx)这种方式去获取,但是Navigation组件还提供给了我们更简单的方式,当你设置了标签后,通过编译代码,会自动为我们生成一个XXXFragmentDirections类,它里面为我们作了参数的封装,而NavController的navigate()方法同时支持direction类型的传递。

val direction = HomeFragmentDirections.actionNavigationFragmentToWebFragment(link,title)
it.findNavController().navigate(direction)

同时在我们的目标页面所对应了一个XXXFragmentArgs,我们可以直接拿到navArgs()从这里我们可以直接拿到参数。

private val args: WebFragmentArgs by navArgs()

2.4 Deep Link

关于Deep Link 是指跳入应用内的一个功能,我就把它翻译成深层链接了,Navigation提供了这样一个功能,使用起来也很简单:

            val args = Bundle()
            args.putString("deep_args",et_deep_link.text.toString())
            val deep = findNavController().createDeepLink()
                .setDestination(R.id.notificationSampleFragment)
                .setArguments(args)
                .createPendingIntent()

            val notificationManager =
                context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 
                notificationManager.createNotificationChannel(
                    NotificationChannel(
                        "deeplink", "Deep Links", NotificationManager.IMPORTANCE_HIGH)
                )
            
            val builder = NotificationCompat.Builder(
                context!!, "deeplink")
                .setContentTitle(resources.getString(R.string.app_name))
                .setContentText("Navigation 深层链接测试")
                .setSmallIcon(R.mipmap.jetpack)
                .setContentIntent(deep)
                .setAutoCancel(true)
            notificationManager.notify(0, builder.build())

我们可以创建一个DeepLink,带上参数,通过Notification通知来测试这样的效果,可以直接跳到项目中的该页面。
具体可查看SampleNotificationFragment

3. 源码解析

3.1 NavHostFragment

官网上是这样介绍它的:NavHostFragment provides an area within your layout for self-contained navigation to occur. 大致意思就是NavHostFragment在布局中提供了一个区域,用于进行包含导航

接下来我们看一下它的源码:

public class NavHostFragment extends Fragment implements NavHost 
    @CallSuper
    @Override
    public void onAttach(@NonNull Context context) 
        super.onAttach(context);
        if (mDefaultNavHost) 
            requireFragmentManager().beginTransaction()
                    .setPrimaryNavigationFragment(this)
                    .commit();
        
    

可以看到它就是一个Fragment,在onAttach生命周期开启事务将它自己设置成了PrimaryFragment了,当然通过defaultNavHost条件判断的,这个布尔值看着眼熟吗?没错,就是我们在xml布局中设置的那一个。

					<fragment
                    android:id="@+id/fragment_home"
                    android:name="androidx.navigation.fragment.NavHostFragment"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    app:defaultNavHost="true"
                    app:navGraph="@navigation/navigation_main"/>

接着看它的onCreate生命周期

    @CallSuper
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        final Context context = requireContext();

        mNavController = new NavController(context);
        mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());

       	.......

        if (navState != null) 
            // Navigation controller state overrides arguments
            mNavController.restoreState(navState);
        
        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);
            
        
    

我们看到在onCreate生命周期中创建了一个NavController,并且为这个NavController创建了一个_Navigator__添加了进去,_我们跟踪createFragmentNavigator,发现它创建了一个FragmentNavigator,这个类是做什么的呢?它继承了Navigator,查看注释我们知道它是为每个Navigation设置策略的,也就是说Fragment之间通过导航切换都是由它来操作的,下面会详细介绍的,这里先简单看下。
接下来我们看到为NavController设置了setGraph(),也就是我们xml里面定义的navGraph,导航布局里面的Fragmentaction跳转等信息。

还有就是onCreateView、onViewCreated等生命周期方法,基本就是加载布局设置ID的方法了。

下面我们跟到NavController.setGraph()中看下是怎样将我们设计的fragment添加进去的?

3.2 NavController

/**
     * Sets the @link NavGraph navigation graph to the specified graph.
     * Any current navigation graph data (including back stack) will be replaced.
     *
     * <p>The graph can be retrieved later via @link #getGraph().</p>
     *
     * @param graph graph to set
     * @see #setGraph(int, Bundle)
     * @see #getGraph
     */
    @CallSuper
    public void setGraph(@NonNull NavGraph graph, @Nullable Bundle startDestinationArgs) 
        if (mGraph != null) 
            // Pop everything from the old graph off the back stack
            popBackStackInternal(mGraph.getId(), true);
        
        mGraph = graph;
        onGraphCreated(startDestinationArgs);
    

我们看如果设置的graph不为null,它执行了popBackStackInternal,看注释的意思为从之前的就的graph栈弹出所有的graph:

boolean popBackStackInternal(@IdRes int destinationId, boolean inclusive) 
        .....
        .....
        boolean popped = false;
        for (Navigator navigator : popOperations) 
            if (navigator.popBackStack()) 
                mBackStack.removeLast();
                popped = true;
             else 
                // The pop did not complete successfully, so stop immediately
                break;
            
        
        return popped;
    

果真remove掉了之前所有的naviagtor。而这个mBackStack是什么时候添加的navigator的呢?查看源码我们发现:

private void navigate(@NonNull NavDestination node, @Nullable Bundle args,
            @Nullable NavOptions navOptions, @Nullable Navigator.Extras navigatorExtras) 
        boolean popped = false;
        if (navOptions != null) 以上是关于1. Jetpack源码解析---看完你就知道Navigation是什么了?的主要内容,如果未能解决你的问题,请参考以下文章

5. Jetpack源码解析---ViewModel基本使用及源码解析

5. Jetpack源码解析---ViewModel基本使用及源码解析

5. Jetpack源码解析---ViewModel基本使用及源码解析

4. Jetpack源码解析—LiveData的使用及工作原理

4. Jetpack源码解析—LiveData的使用及工作原理

4. Jetpack源码解析—LiveData的使用及工作原理