Android Jetpack组件之Navigation使用-源码

1、前言

最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面。

Android Architecture组件是Android Jetpack的一部分,它们是一组库,旨在帮助开发者设计健壮、可测试和可维护的应用程序,包含一下组件:

  • Android Jetpack组件总览
  • Android Jetpack 组件之 Lifecycle使用
  • Android Jetpack 组件之 Lifecycle源码
  • Android Jetpack组件之ViewModel使用
  • Android Jetpack组件之 LiveData使用-源码
  • Android Jetpack组件之 Paging使用-源码
  • Android Jetpack组件之 Room使用-源码
  • Android Jetpack组件之Navigation使用-源码
  • Android Jetpack组件之WorkManger使用介绍
  • Android Jetpack组件App Startup简析
  • Android Jetpack组件之Hilt使用

本系列文章是各处copy过来的,个人感觉所有的开发者都应该尽早的熟悉Jetpack组件,相信一定会被它的魅力所吸引,最近也在完成一个使用以上所有组件实现的项目,作为对Jetpack组件的项目实践,下面来分析一下每个组件对项目开发的帮助。

2、Navigation简介

导航架构组件简化了Android应用程序中导航的实现,通过在xml中添加元素并指定导航的起始和目的地,从而在Fragment之间建立连接在Activity中调用xml中设置的导航action从而跳转界面到目的地,简单来说它和之前在活动中调用startActivity的区别就类似于代码布局和xml中layout布局一样,既简单又可视化,如下图就是一个navigaton的xml图:

Android Jetpack组件之Navigation使用-源码_第1张图片

Navigation多数作用于Fragment中,不过导航组件还支持:Fragment、Activity、导航图和子图、自定义目标。本文内容实现如下功能:

Android Jetpack组件之Navigation使用-源码_第2张图片

3、Navigation实战操作

在实战之前,我们先来了解一下Navigation中最关键的三要素,他们是:

名词 解释
Navigation Graph(New XML resource) 如我们的第一张图所示,这是一个新的资源文件,用户在可视化界面可以看出他能够到达的Destination(用户能够到达的屏幕界面),以及流程关系。
NavHostFragment(Layout XML view) 当前Fragment的容器
NavController(Kotlin/Java object) 导航的控制者

可能我这么解释还是有点抽象,做一个不是那么恰当的比喻,我们可以将Navigation Graph看作一个地图,NavHostFragment看作一个车,以及把NavController看作车中的方向盘,Navigation Graph中可以看出各个地点(Destination)和通往各个地点的路径,NavHostFragment可以到达地图中的各个目的地,但是决定到什么目的地还是方向盘NavController,虽然它取决于开车人(用户)。

第一步 添加依赖

模块层的build.gradle文件需要添加:

ext.navigationVersion = "2.0.0"
dependencies {
    //... 
    implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.navigationVersion"
    implementation "androidx.navigation:navigation-ui-ktx:$rootProject.navigationVersion"
}

如果你要使用SafeArgs插件,还要在项目目录下的build.gradle文件添加:

buildscript {
    ext.navigationVersion = "2.0.0"
    dependencies {
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigationVersion"
    }
}

以及模块下面的build.gradle文件添加:

apply plugin: 'kotlin-android-extensions'
apply plugin: 'androidx.navigation.safeargs'

第二步 创建navigation导航

  1. 创建基础目录:资源文件res目录下创建navigation目录 -> 右击navigation目录New一个Navigation resource file
  2. 创建一个Destination,如果说navigation是我们的导航工具,Destination是我们的目的地,在此之前,我已经写好了一个WelcomeFragmentLoginFragmentRegisterFragment,添加Destination的操作完成后如下所示:

Android Jetpack组件之Navigation使用-源码_第3张图片

除了可视化界面之外,我们仍然有必要看一下里面的内容组成,login_navigation.xml:



    

    
        
        
    

    

        
    

我在这里省略了一些不必要的代码。让我们看一下navigation标签的属性:

属性 解释
app:startDestination 默认的起始位置

第三步 建立NavHostFragment

我们创建一个新的LoginActivity,在activity_login.xml文件中:



    

有几个属性需要解释一下:

属性 解释
android:name 值必须是androidx.navigation.fragment.NavHostFragment,声明这是一个NavHostFragment
app:navGraph 存放的是第二步建好导航的资源文件,也就是确定了Navigation Graph
app:defaultNavHost="true" 与系统的返回按钮相关联

第四步 界面跳转、参数传递和动画

WelcomeFragment中,点击登录和注册按钮可以分别跳转到LoginFragmentRegisterFragment中。

Android Jetpack组件之Navigation使用-源码_第4张图片

这里我使用了两种方式实现:

  • 方式一 利用ID导航

目标:WelcomeFragment携带keyname的数据跳转到LoginFragmentLoginFragment接收后显示。

Have a account ? Login按钮的点击事件如下:

btnLogin.setOnClickListener {
            // 设置动画参数
            val navOption = 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
                }
            }
            // 参数设置
            val bundle = Bundle()
            bundle.putString("name","TeaOf")
            findNavController().navigate(R.id.login, bundle,navOption)
}

后续LoginFragment的接收代码比较简单,直接获取Fragment中的Bundle即可,这里不再出示代码。最后的效果:

Android Jetpack组件之Navigation使用-源码_第5张图片

  • 方式二 利用Safe Args

目标:WelcomeFragment通过Safe Args将数据传到RegisterFragmentRegisterFragment接收后显示。

再看一下已经展示过的login_navigation.xml



    

    
        
        
    

    

        
    

细心的同学可能已经观察到navigation目录下的login_navigation.xml资源文件中的action标签和argument标签,这里需要解释一下:

心的同学可能已经观察到navigation目录下的login_navigation.xml资源文件中的action标签和argument标签,这里需要解释一下:

action标签

属性 作用
app:destination 跳转完成到达的fragment的Id
app:popUpTo fragment中弹出,直到某个Id的fragment

argument标签

属性 作用
android:name 标签名字
app:argType 标签的类型
android:defaultValue 默认值

点击Android studio中的Make Project按钮,可以发现系统为我们生成了两个类:

Android Jetpack组件之Navigation使用-源码_第6张图片

 

WelcomeFragment中的JOIN US按钮点击事件:

btnRegister.setOnClickListener {
            val action = WelcomeFragmentDirections
                .actionWelcomeToRegister()
                .setEMAIL("[email protected]")
            findNavController().navigate(action)
}

RegisterFragment中的接收:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // ...
        val safeArgs:RegisterFragmentArgs by navArgs()
        val email = safeArgs.email
        mEmailEt.setText(email)
}

以及效果:

Android Jetpack组件之Navigation使用-源码_第7张图片

需要提及的是,如果不用Safe Argsaction可以由Navigation.createNavigateOnClickListener(R.id.next_action, null)方式生成,感兴趣的同学可以自行编写。

4、更多

Navigation可以绑定menusdrawersbottom navigation,这里我们以bottom navigation为例,我先在navigation目录下新创建了main_navigation.xml,接着新建了MainActivity,下面则是activity_main.xml:



    

    

MainActivity中的处理也十分简单:

class MainActivity : AppCompatActivity() {

    lateinit var bottomNavigationView: BottomNavigationView

    override fun onCreate(savedInstanceState: Bundle?) {
        //...
        val host: NavHostFragment = supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
        val navController = host.navController
        initWidget()
        initBottomNavigationView(bottomNavigationView,navController)
    }

    private fun initBottomNavigationView(bottomNavigationView: BottomNavigationView, navController: NavController) {
        bottomNavigationView.setupWithNavController(navController)
    }

    private fun initWidget() {
        bottomNavigationView = findViewById(R.id.navigation_view)
    }
}

效果:

Android Jetpack组件之Navigation使用-源码_第8张图片

5、源码

5.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布局中设置的那一个。app:defaultNavHost="true"

接着看它的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,导航布局里面的Fragment及action跳转等信息。

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

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

5.2 NavController

/**
     * Sets the {@link NavGraph navigation graph} to the specified graph.
     * Any current navigation graph data (including back stack) will be replaced.
     *
     * 

The graph can be retrieved later via {@link #getGraph()}.

* * @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) {
            if (navOptions.getPopUpTo() != -1) {
                popped = popBackStackInternal(navOptions.getPopUpTo(),
                        navOptions.isPopUpToInclusive());
            }
        }
        Navigator navigator = mNavigatorProvider.getNavigator(
                node.getNavigatorName());
        Bundle finalArgs = node.addInDefaultArgs(args);
        NavDestination newDest = navigator.navigate(node, finalArgs,
                navOptions, navigatorExtras);
        if (newDest != null) {
            // 如果NavGraph不在栈内,先拿到父类Navgarph
            ArrayDeque hierarchy = new ArrayDeque<>();
            NavGraph parent = newDest.getParent();
            while (parent != null) {
                hierarchy.addFirst(new NavBackStackEntry(parent, finalArgs));
                parent = parent.getParent();
            }
            // 现在遍历后堆栈并查看哪些导航图已经在栈内
            Iterator iterator = mBackStack.iterator();
            while (iterator.hasNext() && !hierarchy.isEmpty()) {
                NavDestination destination = iterator.next().getDestination();
                if (destination.equals(hierarchy.getFirst().getDestination())) {
                    //destination 如果已经在栈顶,不需要再add了
                    hierarchy.removeFirst();
                }
            }
            // Add all of the remaining parent NavGraphs that aren't
            // already on the back stack
            mBackStack.addAll(hierarchy);
            //添加新的 destination
            NavBackStackEntry newBackStackEntry = new NavBackStackEntry(newDest, finalArgs);
            mBackStack.add(newBackStackEntry);
        }
        if (popped || newDest != null) {
            dispatchOnDestinationChanged();
        }
    }

还记得这个方法吗?我们一般手动切换Fragment时可以调用这个方法,最后就是跟踪到这里。

findNavController().navigate(R.id.bottomNavSampleActivity)

同时,切换目标Fragment到栈顶。我们发现最后dispatchOnDestinationChanged()这个方法,分发目标界面切换。有必要去跟一下,你可能会发现意想不到的东西:

 /**
     * Dispatch changes to all OnDestinationChangedListeners.
     * 

* If the back stack is empty, no events get dispatched. * * @return If changes were dispatched. */ @SuppressWarnings("WeakerAccess") /* synthetic access */ boolean dispatchOnDestinationChanged() { // We never want to leave NavGraphs on the top of the stack //noinspection StatementWithEmptyBody while (!mBackStack.isEmpty() && mBackStack.peekLast().getDestination() instanceof NavGraph && popBackStackInternal(mBackStack.peekLast().getDestination().getId(), true)) { // Keep popping } if (!mBackStack.isEmpty()) { NavBackStackEntry backStackEntry = mBackStack.peekLast(); for (OnDestinationChangedListener listener : mOnDestinationChangedListeners) { listener.onDestinationChanged(this, backStackEntry.getDestination(), backStackEntry.getArguments()); } return true; } return false; }

这里面分发了所有实现了OnDestinationChangedListener接口的方法,继续跟踪,看看都哪些实现了这个接口呢?

只有一个类实现了AbstractAppBarOnDestinationChangedListener,看一下具体实现:

@Override
    public void onDestinationChanged(@NonNull NavController controller,
            @NonNull NavDestination destination, @Nullable Bundle arguments) {
        DrawerLayout drawerLayout = mDrawerLayoutWeakReference != null
                ? mDrawerLayoutWeakReference.get()
                : null;
        if (mDrawerLayoutWeakReference != null && drawerLayout == null) {
            controller.removeOnDestinationChangedListener(this);
            return;
        }
        CharSequence label = destination.getLabel();
        if (!TextUtils.isEmpty(label)) {
            ......
            ......
            matcher.appendTail(title);
            //设置title
            setTitle(title);
        }
        boolean isTopLevelDestination = NavigationUI.matchDestinations(destination,
                mTopLevelDestinations);
        if (drawerLayout == null && isTopLevelDestination) {
            //设置icon
            setNavigationIcon(null, 0);
        } else {
            //设置返回箭头状态
            setActionBarUpIndicator(drawerLayout != null && isTopLevelDestination);
        }
    }

原来如此,到这里就应该清楚了,当我们切换Fragment时,大概流程如下:

  • 切换目标fragment到栈顶
  • 分发目标Fragment切换状态
  • 设置toolbar的标题、icon状态等
  • 当然setTitle()、setNavigationIcon()等都为抽象方法,具体实现可以看子类里是怎么实现的,具体就不叙述了

到这里,基本的几个核心类以及相关实现我们基本了解了,下面我们看一下基本的流程,首先我们从入口进去,一点点跟进

5.3 Navigation.findNavController(this, R.id.fragment_home)

我们在最开始会初始化一个NavController:

@NonNull
    public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
        View view = ActivityCompat.requireViewById(activity, viewId);
        NavController navController = findViewNavController(view);
        .......
        return navController;
    }

@Nullable
    private static NavController findViewNavController(@NonNull View view) {
        while (view != null) {
            NavController controller = getViewNavController(view);
            .........
        }
        return null;
    }

@SuppressWarnings("unchecked")
    @Nullable
    private static NavController getViewNavController(@NonNull View view) {
        Object tag = view.getTag(R.id.nav_controller_view_tag);
        NavController controller = null;
        if (tag instanceof WeakReference) {
            controller = ((WeakReference) tag).get();
        } else if (tag instanceof NavController) {
            controller = (NavController) tag;
        }
        return controller;
    }

查看代码可以看到是通过一个tag值来找到的,那么什么时候设置的呢?还记得5.1里面介绍的NavHostFragment的生命周期onViewCreated么?

@Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        .......
        View rootView = view.getParent() != null ? (View) view.getParent() : view;
        Navigation.setViewNavController(rootView, mNavController);
    }

在视图创建的时候调用了Naviagtion.setViewNavController()。NavController初始化好了之后,接下来将它和NavigationView、ToolBar、BottomNavigationView、DrawerLayout进行绑定:

5.4 setupActionBarWithNavController

不管是NavigationView还是Bottom``NavigationView,都会调用这个方法,他是AppCompatActivity的一个扩展方法,调用的是NavigationUI这个类:

public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
            @NonNull NavController navController,
            @NonNull AppBarConfiguration configuration) {
        navController.addOnDestinationChangedListener(
                new ActionBarOnDestinationChangedListener(activity, configuration));
    }

可以看到它就是调用了目标切换的那个接口,用来实现标题按钮等状态的改变。查看它的方法实现:

我们看到它重载了很多方法,包括我们上面提到的NavigationView、ToolBar、BottomNavigationView、DrawerLayout。这样就将组件的状态切换绑定起来了,当fragment切换时,上面提到的接口分发,去切换布局按钮等状态。

5.5 navView.setupWithNavController(navController)

public static void setupWithNavController(@NonNull final NavigationView navigationView,
            @NonNull final NavController navController) {
        navigationView.setNavigationItemSelectedListener(
                new NavigationView.OnNavigationItemSelectedListener() {
                    @Override
                    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                        //目标页面是否被选中
                        boolean handled = onNavDestinationSelected(item, navController);
                        if (handled) {
                            //切换菜单状态、关闭抽屉
                            ViewParent parent = navigationView.getParent();
                            if (parent instanceof DrawerLayout) {
                                ((DrawerLayout) parent).closeDrawer(navigationView);
                            } else {
                                BottomSheetBehavior bottomSheetBehavior =
                                        findBottomSheetBehavior(navigationView);
                                if (bottomSheetBehavior != null) {
                                    bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
                                }
                            }
                        }
                        return handled;
                    }
                });
        final WeakReference weakReference = new WeakReference<>(navigationView);
        navController.addOnDestinationChangedListener(
                new NavController.OnDestinationChangedListener() {
                    @Override
                    public void onDestinationChanged(@NonNull NavController controller,
                            @NonNull NavDestination destination, @Nullable Bundle arguments) {
                        NavigationView view = weakReference.get();
                        if (view == null) {
                            navController.removeOnDestinationChangedListener(this);
                            return;
                        }
                        Menu menu = view.getMenu();
                        for (int h = 0, size = menu.size(); h < size; h++) {
                            MenuItem item = menu.getItem(h);
                            item.setChecked(matchDestination(destination, item.getItemId()));
                        }
                    }
                });
    }

最后就是状态切换了,当点击menu菜单或者目标Fragment切换的时候,改变状态。

6、 流程

考虑到我们开始如果直接从setupWithNavController 入口进行分析的话,可能不太容易找到怎么创建的graph布局中的fragment,以及NavHostFragment到底是什么,所以我们先分析了布局中的**NavHostFragment,我们发现为什么要在布局中声明了一个NavHostFragment,**它是用来做什么的,最后发现在它的生命周期中创建了一个NavController,并且添加了FragmentNavigator,同时setGraph了。

紧接着我们通过setGraph进入到了NavController类中,通过graph里面设置的初始fragment看到了切换栈内切换Fragment的代码。

在里面我们看到了熟悉的navigate()方法,在里面dispatchOnDestinationChanged()吸引了我的注意力,通过查找,发现切换Fragment之后,通过该方法去改变布局的状态,也就是OnDestinationChangedListener接口。

到这里基本的代码实现已经了解的差不多了,然后我回到了入口,通过初始化NavController,调用NavigationUI中的方法绑定NavigationView、ToolBar、BottomNavigationView、DrawerLayout等布局,在调用navigate()方法后,改变状态,整个流程就走通了。

可能有一些不合理的地方,望大家见谅,但是这是我此次的一个基本流程。


重要参考

即学即用Android Jetpack - Navigation

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

你可能感兴趣的:(Kotlin,android,kotlin,jetpack)