Android navigation的简单理解和使用

  • 1 、基本概念
    • 1.1、背景
    • 1.2、含义
  • 2、组成
    • 2.1、Navigation graph
    • 2.2、NavHostFragment
    • 2.3、NavController
  • 3、基本使用
    • 3.1、引入依赖
    • 3.2、创建导航视图
    • 3.3、配置graph:添加fragment
    • 3.4、添加NavHostFragment
    • 3.5、通过NavController 管理fragment之间的跳转
      • 3.5.1、NavController 的获取及其能力
  • 4、跳转时传递参数
    • 4.1、通过带 bundle 参数的 navigate 方法传递参数
    • 4.2、通过 safeArgs 插件
  • 5、动画
    • 5.1、action 参数设置动画
    • 5.2、共享元素
  • 6、常见问题
    • 1、Fragment 跳转的启动类型 (singleTask、singleTop) 如何提供支持?
    • 2、Fragment 之间的如何通信?
    • 3、navigation fragment的重绘
      • 3.1、Fragment生命周期
  • 参考

1 、基本概念

1.1、背景

采用单个Activity嵌套多个Fragment的UI架构模式,已经被大多数的Android工程师所接受,需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。

在Android中,页面的切换和管理包括应用程序Appbar的管理、Fragment的动画切换以及Fragment之间的参数传递等内容。并且,纯代码的方式使用起来不是特别友好,并且Appbar在管理和使用的过程中显得很混乱。因此,Jetpack提供了一个名为Navigation的组件,旨在方便开发者管理Fragment页面和Appbar。

相比之前Fragment的管理需要借助FragmentManagerFragmentTransaction,使用Navigation组件有如下一些优点:

  • 可视化的页面导航图,方便我们理清页面之间的关系
  • 通过destination和action完成页面间的导航
  • 方便添加页面切换动画
  • 页面间类型安全的参数传递
  • 通过Navigation UI类,对菜单/底部导航/抽屉蓝菜单导航进行统一的管理
  • 支持深层链接DeepLink

1.2、含义

  • Navigation 是 Android Jetpack 组件包 中的重要一员,借助于 Single Activity 和 多个Fragment 碎片,优化 Android Activity 启动的开销和简化 Activity 之间的数据通信问题。
  • 内置支持普通 Fragment、Activity 和 DialogFragment 组件的跳转,也就是所有 Dialog 或PopupWindow 都建议使用 DialogFragment 实现,这样可以涵盖所有常用的跳转场景,统一返回栈的管理。
  • 另外,基于 Fragment 实现可以做到状态存储和恢复。

假设你是一名传统的基于 Activity 开发者,现在想迁移到 Navigation 导航架构,你一定会下面几个疑问:

  • 全都用 Fragment?那原本 Activity 跳转的启动类型 (singleTask、singleTop) 如何提供支持?
  • Fragment 之间的如何传递数据?
  • 原本 onActivityForResult 现在应该如何实现一个 “onFragmentResult” ?

2、组成

2.1、Navigation graph

一个包含所有导航相关信息的 XML 资源

  • xml 档,包含所有被管理的 Fragment,起始目标,换页目标,返回目标。

2.2、NavHostFragment

一种特殊的Fragment,用于承载导航内容的容器

<fragmentandroid:id="@+id/navHostFragment"android:name="androidx.navigation.fragment.NavHostFragment"android:layout_width="match_parent"android:layout_height="match_parent"app:defaultNaHost="true"app:navGraph="@navigation/nav_graph_main" />

2.3、NavController

管理应用导航的对象,实现Fragment之间的跳转等操作

  • 用来管理 NavHost 中的导航动作,通常是写在点击事件内完成 Fragment 的切换。
textView.setOnClickListener {findNavController().navigate(R.id.action_Fragment1_to_Fragment2)}

3、基本使用

3.1、引入依赖

implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'

3.2、创建导航视图

首先确保AndroidStudio为3.3以上

  • 1.右键res,点击New -> Android Resource Directory
  • 2.在出现的面板第二行Resource type 下拉列表中选择 Navigation,然后点击 OK
  • 3.res目录下会多出一个navigation的资源目录,右键该目录,点击New -> Navigation Resource File,输入需要新建的资源文件名,这里命名nav_graph,点击ok,一个nav_graph.xml就创建好了。

3.3、配置graph:添加fragment

新建好的nav_graph.xml切换到design模式下,点击2处的加号,选择Create new destination,即可快速创建新的Fragment,这里分别新建了FragmentAFragmentBFragmentC三个fragment
建好后,可通过手动配置页面之间的跳转关系,点击某个页面,右边会出现一个小圆点,拖曳小圆点指向跳转的页面,这里设置跳转的关系为FragmentA -> FragmentB -> FragmentC
切换到Code栏,可以看到生成了如下代码

<?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/fragmentA"><fragmentandroid:id="@+id/fragmentA"android:name="com.example.testnavigation.FragmentA"android:label="fragment_a"tools:layout="@layout/fragment_a" ><actionandroid:id="@+id/action_fragmentA_to_fragmentB2"app:destination="@id/fragmentB" /></fragment><fragmentandroid:id="@+id/fragmentB"android:name="com.example.testnavigation.FragmentB"android:label="fragment_b"tools:layout="@layout/fragment_b" ><actionandroid:id="@+id/action_fragmentB_to_fragmentC2"app:destination="@id/fragmentC" /></fragment><fragmentandroid:id="@+id/fragmentC"android:name="com.example.testnavigation.FragmentC"android:label="fragment_c"tools:layout="@layout/fragment_c" />
</navigation>
  • navigation是根标签,通过startDestination配置默认启动的第一个页面,这里配置的是FragmentA
  • fragment标签代表一个fragment,其实这里不仅可以配置fragment,也可以配置activity,甚至还可以自定义
  • action标签定义了页面跳转的行为,相当于上图中的每条线,destination定义跳转的目标页,还可以定义跳转时的动画等等
    • 当调用到 action_FragmentA_to_FragmentB2 这个 action,会从 FragmentA -> FragmentB

3.4、添加NavHostFragment

在MainActivity的布局文件中配置NavHostFragment

<?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"><fragmentandroid:id="@+id/fragment"android:name="androidx.navigation.fragment.NavHostFragment"android:layout_width="match_parent"android:layout_height="match_parent"app:defaultNavHost="true"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent"app:navGraph="@navigation/nav_graph" /></androidx.constraintlayout.widget.ConstraintLayout>
  • android:name指定NavHostFragment
  • app:navGraph指定导航视图,即建好的nav_graph.xml
  • app:defaultNavHost=true意思是可以拦截系统的返回键,可以理解为默认给fragment实现了返回键的功能,这样在fragment的跳转过程中,当我们按返回键时,就可以使得fragment跟activity一样可以回到上一个页面了

现在我们运行程序,就可以正常跑起来了,并且看到了FragmentA展示的页面,这是因为MainActivity的布局文件中配置了NavHostFragment,并且给NavHostFragment指定了导航视图,而导航视图中通过startDestination指定了默认展示FragmentA。

3.5、通过NavController 管理fragment之间的跳转

上面说到三个fragment之间的跳转关系是FragmentA -> FragmentB -> FragmentC,并且已经可以展示了FragmentA,那怎么跳转到FragmentB呢,这就需要用到NavController 了
打开FragmentA类,给布局中的TextView定义一个点击事件

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)tv.setOnClickListener {val navController = Navigation.findNavController(it)navController.navigate(R.id.action_fragmentA_to_fragmentB2)}
}

如果发现不能自动导入布局文件,大概率是要给app.build添加插件‘kotlin-android-extensions’

plugins {id 'com.android.application'id 'kotlin-android'id 'kotlin-android-extensions'
}

可以看到,通过navController管理fragment的跳转非常简单,首先得到navController对象,然后调用它的navigate方法,传入前面nav_graph中定义的action的id即可。

按同样的方法给FragmentB中的TextView也设置一个点击事件,使得点击时跳转到FragmentC

运行程序,FragmentA -> FragmentB -> FragmentC,此时按返回键,也是一个一个页面返回,如果把前面的app:defaultNavHost设置为false,按返回键后会发现直接返回到桌面。

3.5.1、NavController 的获取及其能力

上面的例子中,我们通过 Fragment 的扩展方法可以拿到此 Fragment 从属的 NavController,另外还有一些重载的方法:

// 根据 viewId 向上查找
NavController findNavController(Activity activity, int viewId)
// 根据 view 向上查找
NavController findNavController(View view)

本质上 findNavController 就是在当前 view 树中,查找距离指定 view 最近的父 NavHostFragment 对应的 NavController,目前仅做了解即可。

NavController 的能力

对于应用层来说,整个 Navigation 框架,我们只跟 NavController 打交道,它提供了常用的跳转、返回和获取返回栈等能力。

4、跳转时传递参数

4.1、通过带 bundle 参数的 navigate 方法传递参数

通过指定 bundle 参数可以为目的地传递参数,比如:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)tv.setOnClickListener {val navController = Navigation.findNavController(it)val bundle = Bundle()bundle.putString("key", "test")navController.navigate(R.id.action_fragmentA_to_fragmentB2, bundle)}
}

在目的地 Fragment 可以直接通过 getArguments() 方法获取 这个bundle。

    super.onCreate(savedInstanceState)val value = arguments?.getString("key")...
}

4.2、通过 safeArgs 插件

afe args与传统传参方式相比,好处在于安全的参数类型,并且通过谷歌官方的支持,能很方便的进行参数传值。

1、在项目的根build.gradle下添加插件

buildscript {ext.kotlin_version = "1.3.72"repositories {google()jcenter()}dependencies {classpath "com.android.tools.build:gradle:7.0.4"classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.5.0"}
}allprojects {repositories {google()jcenter()}
}

2、然后在app的build.gradle中引用 'androidx.navigation.safeargs.kotlin'

    id 'com.android.application'id 'kotlin-android'id 'kotlin-android-extensions'id 'androidx.navigation.safeargs.kotlin'
}

3、添加完插件后,回到nav_graph,切到design模式,给目标页面添加需要接收的参数

这里需要在FragmentA跳转到FragmentB时传参数,所以给FragmentB设置参数,点击FragmentB,点击右侧面板的Arguments右侧的+,输入参数的key值,指定参数类型和默认值,即可快速添加参数
4、添加完后,rebuild一下工程,safeArgs会自动生成一些代码,在/build/generated/source/navigation-args目录下可以看到
safeArgs会根据nav_graph中的fragment标签生成对应的类,

  • action标签会以“类名+Directions”命名,
  • argument标签会以“类名+Args”命名。

使用safeArgs后,传递参数是这样的

 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)tv.setOnClickListener {val navController = Navigation.findNavController(it)//通过safeArgs传递参数val navDestination = FragmentADirections.actionFragmentAToFragmentB2("test")navController.navigate(navDestination)// 普通方式传递参数// val bundle = Bundle()// bundle.putString("key", "test")// navController.navigate(R.id.action_fragmentA_to_fragmentB2, bundle)}}

接收参数是这样的

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)arguments?.let {val value = FragmentBArgs.fromBundle(it).key.......}.......}

5、动画

5.1、action 参数设置动画

enterAnim: 跳转时的目标页面动画exitAnim: 跳转时的原页面动画popEnterAnim: 回退时的目标页面动画popExitAnim:回退时的原页面动画

新增 anim

slide_in_right.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><translateandroid:duration="500"android:fromXDelta="100%"android:fromYDelta="0%"android:toXDelta="0%"android:toYDelta="0%" />
</set>

slide_out_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><translateandroid:duration="500"android:fromXDelta="0%"android:fromYDelta="0%"android:toXDelta="-100%"android:toYDelta="0%" />
</set>

slide_in_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><translateandroid:duration="500"android:fromXDelta="-100%"android:fromYDelta="0%"android:toXDelta="0%"android:toYDelta="0%" />
</set>

slide_out_right.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"><translateandroid:duration="500"android:fromXDelta="0%"android:fromYDelta="0%"android:toXDelta="100%"android:toYDelta="0%" />
</set>

添加到action: 可以根据不同需求使用 alpha、scale、rotate、translate 這几种效果

<actionandroid:id="@+id/action_page1_to_action_page2"app:destination="@id/page2Fragment"app:enterAnim="@anim/slide_in_right"app:exitAnim="@anim/slide_out_left"app:popEnterAnim="@anim/slide_in_left"app:popExitAnim="@anim/slide_out_right" />

5.2、共享元素

如果兩個頁面有類似的元素,可以用這種方式讓視覺有連續被帶過去的感覺。

在兩個頁面共用的元件加上 transitionName 這個屬性,屬性的值要一樣。
fragment_one.xml

<ImageViewandroid:id="@+id/catImageView"android:layout_width="200dp"android:layout_height="200dp"android:src="@mipmap/cat"android:transitionName="catImage" /><TextViewandroid:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="cat"android:transitionName="catText" />

fragment_two.xml

<ImageViewandroid:id="@+id/catImageView"android:layout_width="match_parent"android:layout_height="200dp"android:src="@mipmap/cat"android:transitionName="catImage" /><TextViewandroid:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="cat"android:transitionName="catText" />

PageOneFragment.kt
把 xml 元件的 transitionName 赋值给 NavController

val extras = FragmentNavigatorExtras(catImageView to "catImage",textView to "catText")catImageView.setOnClickListener {findNavController().navigate(R.id.action_page1_to_action_page2,null,null, extras)}

PageTwoFragment.java

@Overridepublic void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setSharedElementEnterTransition( TransitionInflater.from(requireContext()).inflateTransition(R.transition.shared_image));}

shared_image.xml

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"><changeBounds/><changeImageTransform/>
</transitionSet>

6、常见问题

1、Fragment 跳转的启动类型 (singleTask、singleTop) 如何提供支持?

栈管理:点击destination,右侧面板中还可以看到popUpTopopUpToInclusivelaunchSingleTop

1、launchSingleTop:如果栈中已经包含了指定要跳转的界面,那么只会保留一个,不指定则栈中会出现两个界面相同的Fragment数据,可以理解为类似activity的singleTop,即栈顶复用模式,但又有点不一样,比如FragmentA@1 -> FragmentA@2,FragmentA@1会被销毁,但如果是FragmentA@01>FragmentB@02>FragmentA@03FragmentA@1不会被销毁。

2、popUpTo(tag):表示跳转到某个tag,并将tag之上的元素出栈。

3、popUpToInclusive:为true表示会弹出tag,false则不会

例子:FragmentA -> FragmentB -> FragmentC -> FragmentA

设置FragmentC -> FragmentA 的action为popUpTo=FragmentA ,popUpToInclusive=false,那么栈内元素变化为
最后会发现需要按两次返回键才会回退到桌面

设置popUpToInclusive=true时,栈内元素变化为
此时只需要按一次返回键就回退到桌面了,从中可以体会到popUpTo和popUpToInclusive的含义了吧。

2、Fragment 之间的如何通信?

Fragment 中的通信还可以分为两种场景,假设目前 返回栈中有两个Fragment 分别为 A 和 B。

  • 若 A 与 B 在同级子图中,可以在两端通过创建 导航图级别的 ViewModel 完成交互。

例如当前返回栈为

NavGraphA -> NavDestinationB -> NavDestinationC -> NavDestinationD

1、若想实现 C 与 D 的通信,需要使用 可以使用 节点B 创建 ViewModel。

val vm by navGraphViewModels<TitleVm>(R.id.nav_destination_b)

R.id.home 为二者的最近的公共父 Graph,在父 Graph 销毁前,二者通信都是有效的。

2、若 A 与 B 不在同级子图中,可以使用距离二者最近的公共父 Graph 完成通信。

例如当前返回栈为

NavGraphA -> NavDestinationB -> NavGraphC -> NavDestinationD

若想实现 B 与 D 的通信,需要使用 A节点创建 ViewModel。

val vm by navGraphViewModels<TitleVm>(R.id.home)

3、navigation fragment的重绘

3.1、Fragment生命周期

Navigation出现之前官方给出的Fragment生命周期如下图:(注意onDestroyView之处)
而LIfecycle,Navigation等组件出现之后,官方给出的Fragment生命周期图为下图:(PS:Fragment Lifecycle && View Lifecycle)
Navigation框架下的Fragment生命周期分为 Fragment LifecycleView Lifecycle ,View Lifecycle被单独拎出来了,原因就在于Navigation框架下的非栈顶的Fragment均会被销毁View, 也即是 A跳转到B页面: A会执行onDestroyView销毁其 View (凡是和View相关的,如:Databinding、RecyclerView都会被销毁) , 但是Fragment本身会存在( Fragment本身的成员变量等 是不会被销毁的 )

Navigation框架之下的正确状态流转应该是类似这的:

A 通过action打开B,A从 onResume转到onDestroyView,B从onAttach执行到onResume, 当B通过系统返回键返回到A时候,A从上图的onCreateView流转到onResume , 此过程中A的View经历销毁和重建,View(binding实例)的对象实例是不一样的,但是Fragment A这个实例始终相同。

这样的场景下,假设A存在一个网络新闻列表RecyclerView, RecyclerView随着View被销毁、重建。如何保存其中的数据,避免每次返回到A的时候重新刷新数据(造成:上次浏览数据、位置丢失、额外的网络资源消耗), 因此RecyclerView中Adapter的数据项非常关键!

常见的保存方式有:

  • 1、通过Fragment的成员变量
  • 2、ViewModel。在ViewModel的ViewModelScope通过协程请求网络数据,保存在ViewModel(ViewModel生命周期贯穿Fragment),可通过LiveData、普通变量保存数据,在onViewCreated之后恢复数据

参考

1、安卓navigation系列——入门篇
2、安卓navigation系列——进阶篇
3、Navigation 组件使用入门
4、Android官方架构组件Navigation:大巧不工的Fragment管理框架
5、Navigation-02-Fragment生命周期
6、Fragment 重建现象

Android:安卓学习笔记之navigation的简单理解和使用相关推荐

  1. Android:安卓学习笔记之Bitmap的简单理解和使用

    Android Bitmap的简单理解和使用 Android Bitmap 一.Bitmap的定义 二.Bitmap的格式 2.1 存储格式 2.2 压缩格式 三.Bitmap创建方法 3.1 Bit ...

  2. 安卓开发Android studio学习笔记12:读取解析XML(案例演示)

    Android studio学习笔记 第一步:配置Student.XML 第二步:配置activity_main.xml 第三步:配置student.xml 第四步:配置Student用户类 第五步: ...

  3. Android:日常学习笔记(8)———探究UI开发(5)

    Android:日常学习笔记(8)---探究UI开发(5) ListView控件的使用 ListView概述 A view that shows items in a vertically scrol ...

  4. Android Studio --- [学习笔记]RadioButton、CheckBox、ImageView、ListView、TCP的三次握手

    说明 源代码 在2.x里有TCP的三次挥手与四次握手,先对它进行简单的回答(百度).预计在下一篇里,会继续说明TCP 接上一篇: Android Studio - > [学习笔记]Button. ...

  5. 2020年安卓学习笔记目录

    文章目录 一.讲课笔记 二.安卓案例 三.安卓实训项目 四.学生安卓学习博客 五.安卓课后作业 (一)界面设计练习 1.制作登录界面 2.制作部队管理界面 3.制作灭火救援界面 4.制作交付界面 5. ...

  6. java/android 设计模式学习笔记(1)--- 单例模式

    前段时间公司一些同事在讨论单例模式(我是最渣的一个,都插不上嘴 T__T ),这个模式使用的频率很高,也可能是很多人最熟悉的设计模式,当然单例模式也算是最简单的设计模式之一吧,简单归简单,但是在实际使 ...

  7. Android Binder 学习笔记

    前言: Binder是Android给我们提供的一种跨进程通信方式.理解Binder能帮助我们更好的理解Android的系统设计,比如说四大组件,AMS,WMS等系统服务的底层通信机制就都是基于Bin ...

  8. Android:日常学习笔记(8)———探究UI开发(2)

    Android:日常学习笔记(8)---探究UI开发(2) 对话框 说明: 对话框是提示用户作出决定或输入额外信息的小窗口. 对话框不会填充屏幕,通常用于需要用户采取行动才能继续执行的模式事件. 提示 ...

  9. Android Studio --- [学习笔记]TCP(第2弹)、GridView、ScrollView

    说明 这篇主要接上一篇Android Studio - > [学习笔记]RadioButton.CheckBox.ImageView.ListView.TCP的三次握手 对上面回答的细解,并用J ...

最新文章

  1. 逆生长!小鼠「逆龄疗法」登Nature子刊,有望用于人类
  2. 用python开发的网站多吗-django可以开发大型网站吗
  3. 组建Livebos超级快速开发平台学习研讨QQ群 !
  4. rest post put_REST / HTTP方法:POST与PUT与PATCH
  5. git 发布android 系统版本 修改版本型号 查看指定文件的修改记录
  6. python惰性求值的特点_C#教程之C#函数式编程中的惰性求值详解
  7. .net 事务处理的三种方法
  8. Android存储-SharedPreferences
  9. 【android自定义控件】自定义Toast,AlterDialog,Notification 四
  10. python去年软件排行_2016 年有哪些好的 Python 机器学习开源项目?
  11. 简单个人网页设计作业 静态HTML个人博客主页 DW个人网站模板下载 大学生简单个人网页作品代码 个人网页制作 学生个人网页设计作业
  12. Nodejs 国内镜像源加速下载
  13. n元均匀直线matlab,均匀直线阵天线的分析
  14. 宿主机支持avx2指令集,为什么虚拟机cpu就不支持avx2指令集了
  15. win10关机后cpu风扇还在转_win10系统关机后风扇还转的解决方法
  16. NOIP常考模板粗略集合包
  17. cv个人计算机SCI英文简历模板,关于英语简历范文
  18. `include “uvm_macros.svh“引发的思考
  19. 浙江大学计算机学院博士论文格式,博士学位论文格式模板(浙江大学博士论文模板样例)...
  20. 【数据结构】小项目:航班查询系统

热门文章

  1. mac系统python配置
  2. linux shell 删除key \xAC\xED\x00\x05t\x00\x04${key} 序列化16进制
  3. TensorBoard可视化高维向量
  4. 程序员2天做出的猫咪情绪识别软件,究竟用了什么技术?
  5. python+短信宝实现手机短信发送
  6. ftl不存在为真_当两个物体各自以1/2光速运动,朝对方移动,是否可以认为这两个物体在以光速接近?...
  7. todo清单项目开发,todo清单不止是简单的勾选,还能做更多事情
  8. 深度学习实例第四部分:PaddlePaddle
  9. 菜鸟程序员如何才能快速提高自己的技术
  10. iOS 未安装微信,审核被拒绝的解决方式