Android navigation的简单理解和使用
采用单个Activity嵌套多个Fragment的UI架构模式,已经被大多数的Android工程师所接受,需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。
在Android中,页面的切换和管理包括应用程序Appbar的管理、Fragment的动画切换以及Fragment之间的参数传递等内容。并且,纯代码的方式使用起来不是特别友好,并且Appbar在管理和使用的过程中显得很混乱。因此,Jetpack提供了一个名为Navigation的组件,旨在方便开发者管理Fragment页面和Appbar。
相比之前Fragment的管理需要借助FragmentManager和FragmentTransaction,使用Navigation组件有如下一些优点:

假设你是一名传统的基于 Activity 开发者,现在想迁移到 Navigation 导航架构,你一定会下面几个疑问:
一个包含所有导航相关信息的 XML 资源
一种特殊的Fragment,用于承载导航内容的容器
<fragment
android: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" />
管理应用导航的对象,实现Fragment之间的跳转等操作
textView.setOnClickListener {
findNavController().navigate(R.id.action_Fragment1_to_Fragment2)
}
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0'
首先确保AndroidStudio为3.3以上
res,点击New -> Android Resource DirectoryResource type 下拉列表中选择 Navigation,然后点击 OKnavigation的资源目录,右键该目录,点击New -> Navigation Resource File,输入需要新建的资源文件名,这里命名nav_graph,点击ok,一个nav_graph.xml就创建好了。
新建好的nav_graph.xml切换到design模式下,点击2处的加号,选择Create new destination,即可快速创建新的Fragment,这里分别新建了FragmentA、FragmentB、FragmentC三个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">
<fragment
android:id="@+id/fragmentA"
android:name="com.example.testnavigation.FragmentA"
android:label="fragment_a"
tools:layout="@layout/fragment_a" >
<action
android:id="@+id/action_fragmentA_to_fragmentB2"
app:destination="@id/fragmentB" />
</fragment>
<fragment
android:id="@+id/fragmentB"
android:name="com.example.testnavigation.FragmentB"
android:label="fragment_b"
tools:layout="@layout/fragment_b" >
<action
android:id="@+id/action_fragmentB_to_fragmentC2"
app:destination="@id/fragmentC" />
</fragment>
<fragment
android:id="@+id/fragmentC"
android:name="com.example.testnavigation.FragmentC"
android:label="fragment_c"
tools:layout="@layout/fragment_c" />
</navigation>
navigation是根标签,通过startDestination配置默认启动的第一个页面,这里配置的是FragmentAfragment标签代表一个fragment,其实这里不仅可以配置fragment,也可以配置activity,甚至还可以自定义action标签定义了页面跳转的行为,相当于上图中的每条线,destination定义跳转的目标页,还可以定义跳转时的动画等等
在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">
<fragment
android: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指定NavHostFragmentapp:navGraph指定导航视图,即建好的nav_graph.xmlapp:defaultNavHost=true意思是可以拦截系统的返回键,可以理解为默认给fragment实现了返回键的功能,这样在fragment的跳转过程中,当我们按返回键时,就可以使得fragment跟activity一样可以回到上一个页面了现在我们运行程序,就可以正常跑起来了,并且看到了FragmentA展示的页面,这是因为MainActivity的布局文件中配置了NavHostFragment,并且给NavHostFragment指定了导航视图,而导航视图中通过startDestination指定了默认展示FragmentA。
上面说到三个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,按返回键后会发现直接返回到桌面。
上面的例子中,我们通过 Fragment 的扩展方法可以拿到此 Fragment 从属的 NavController,另外还有一些重载的方法:
// 根据 viewId 向上查找
NavController findNavController(Activity activity, int viewId)
// 根据 view 向上查找
NavController findNavController(View view)
本质上 findNavController 就是在当前 view 树中,查找距离指定 view 最近的父 NavHostFragment 对应的 NavController,目前仅做了解即可。
NavController 的能力
对于应用层来说,整个 Navigation 框架,我们只跟 NavController 打交道,它提供了常用的跳转、返回和获取返回栈等能力。


通过指定 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")
...
}
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标签生成对应的类,
“类名+Directions”命名,“类名+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
.......
}
.......
}
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">
<translate
android: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">
<translate
android: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">
<translate
android: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">
<translate
android:duration="500"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:toXDelta="100%"
android:toYDelta="0%" />
</set>
添加到action: 可以根据不同需求使用 alpha、scale、rotate、translate 這几种效果
<action
android: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" />
如果兩個頁面有類似的元素,可以用這種方式讓視覺有連續被帶過去的感覺。
在兩個頁面共用的元件加上 transitionName 這個屬性,屬性的值要一樣。
fragment_one.xml
<ImageView
android:id="@+id/catImageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@mipmap/cat"
android:transitionName="catImage" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="cat"
android:transitionName="catText" />
fragment_two.xml
<ImageView
android:id="@+id/catImageView"
android:layout_width="match_parent"
android:layout_height="200dp"
android:src="@mipmap/cat"
android:transitionName="catImage" />
<TextView
android: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
@Override
public 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>
栈管理:点击destination,右侧面板中还可以看到popUpTo、popUpToInclusive、launchSingleTop

1、launchSingleTop:如果栈中已经包含了指定要跳转的界面,那么只会保留一个,不指定则栈中会出现两个界面相同的Fragment数据,可以理解为类似activity的singleTop,即栈顶复用模式,但又有点不一样,比如FragmentA@1 -> FragmentA@2,FragmentA@1会被销毁,但如果是FragmentA@01>FragmentB@02>FragmentA@03,FragmentA@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的含义了吧。
Fragment 中的通信还可以分为两种场景,假设目前 返回栈中有两个Fragment 分别为 A 和 B。
例如当前返回栈为
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)
Navigation出现之前官方给出的Fragment生命周期如下图:(注意onDestroyView之处)
而LIfecycle,Navigation等组件出现之后,官方给出的Fragment生命周期图为下图:(PS:Fragment Lifecycle && View Lifecycle)
Navigation框架下的Fragment生命周期分为 Fragment Lifecycle 和 View 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的数据项非常关键!
常见的保存方式有:
Fragment的成员变量onViewCreated之后恢复数据1、安卓navigation系列——入门篇
2、安卓navigation系列——进阶篇
3、Navigation 组件使用入门
4、Android官方架构组件Navigation:大巧不工的Fragment管理框架
5、Navigation-02-Fragment生命周期
6、Fragment 重建现象
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po