博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android Jetpack组件之Navigation使用-源码
阅读量:164 次
发布时间:2019-02-28

本文共 19154 字,大约阅读时间需要 63 分钟。

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图:

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

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的操作完成后如下所示:

除了可视化界面之外,我们仍然有必要看一下里面的内容组成,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中。

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

  • 方式一 利用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即可,这里不再出示代码。最后的效果:

  • 方式二 利用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按钮,可以发现系统为我们生成了两个类:

 

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

btnRegister.setOnClickListener {            val action = WelcomeFragmentDirections                .actionWelcomeToRegister()                .setEMAIL("TeaOf1995@Gamil.com")            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)}

以及效果:

需要提及的是,如果不用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)    }}

效果:

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()方法后,改变状态,整个流程就走通了。

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


重要参考

转载地址:http://iauj.baihongyu.com/

你可能感兴趣的文章