Jetpack之-Navigation
MVVM架构,其中使用的Jetpack组件包括Paging、LiveData、ViewModel以及Room等,后面到了现在的公司就一直没有怎么使用过。但Jetpack组件作为谷歌的亲儿子项目,而且随着Compose 1.0稳定版的发布,精通Jetpack全组件开发在未来的趋势是不可逆的了,无论你是原生开发还是做跨平台。总而言之,未来还要做Android开发就得懂使用Jetpack组件协同开发构建应用。
Navigation的定义
Navigation就如它英文的字面意思是导航作用的,它是一个可简化Android导航的组件,多数是用在管理Fragment的切换,另外还支持Activity、导航图和子图、自定义目标,而且在studio中可通过可视化的方式,看见App的交互流程:
本文主要介绍Navigation在Fragment管理方面的应用
Navigation的基本使用
对于一个新的知识点,我们还是通过简单的使用去了解它。
添加Navigation组件库的依赖
implementation "androidx.navigation:navigation-fragment-ktx:2.4.2"
implementation "androidx.navigation:navigation-ui-ktx:2.4.2"
创建三个Fragment以备用
新建三个Fragment,依次是FragmentA、FragmentB、FragmentC,:
class FragmentA : Fragment() {
val tv_a_b by bindView<TextView>(R.id.tv_a_b)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_a,container,false)
}
}
class FragmentB : Fragment() {
val tv_b_c by bindView<TextView>(R.id.tv_b_c)
val tv_b_a by bindView<TextView>(R.id.tv_b_a)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_b,container,false)
}
}
class FragmentC : Fragment() {
val tv_c_b by bindView<TextView>(R.id.tv_c_b)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_c,container,false)
}
}
这里控件初始化使用的是bindView方法,这是个自定义对控件进行延时初始化的方法,只有在第一次使用到控件才会初始化它,这里把封装的方法分享出来:
fun <V : View> Activity.bindView(id: Int): Lazy<V?> = lazy {
viewFindId(id)?.saveAs<V>()
}
val viewFindId: Activity.(Int) -> View?
get() = { findViewById(it) }
fun <V : View> Fragment.bindView(id: Int): Lazy<V?> = lazy {
frgViewFindId(id)?.saveAs<V>()
}
val frgViewFindId: Fragment.(Int) -> View?
get() = { view?.findViewById(it) }
fun <T> Any.saveAs():T?{
return this as? T
}
创建Navigation的xml文件
首先我们在res文件夹下创建一个navigation文件夹,这里要注意在新建文件夹的时要选择Android Resource Directory,不然新建出来的文件右键创建Navigation文件的时候没有Navigation Resource File一项.
在新建好的navigation文件夹右键new中选择Navigation Resource File创建一个名为
jetpack_navigation的xml文件.在
jetpack_navigation.xml中配置创建的3个Fragment:
<?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/jetpack_navigation"
app:startDestination="@id/fragment_a">
<fragment
android:id="@+id/fragment_a"
android:name="com.transocks.myapplication.FragmentA"
android:label="fragment_a"
tools:layout="@layout/fragment_a" >
<action
android:id="@+id/action_fragment_a_to_b"
app:destination="@id/fragment_b" />
</fragment>
<fragment
android:id="@+id/fragment_b"
android:name="com.transocks.myapplication.FragmentB"
android:label="fragment_b"
tools:layout="@layout/fragment_b" >
<action
android:id="@+id/action_fragment_b_to_c"
app:destination="@id/fragment_c" />
<action
android:id="@+id/action_fragment_b_to_a"
app:destination="@id/fragment_a" />
</fragment>
<fragment
android:id="@+id/fragment_c"
android:name="com.transocks.myapplication.FragmentC"
android:label="fragment_c"
tools:layout="@layout/fragment_c" >
<action
android:id="@+id/action_fragment_c_to_b"
app:destination="@id/fragment_b" />
</fragment>
</navigation>
这里有一点要注意的是在<fragment>标签中要有id,name,label、layout这四个属性的:
id: 当前<
fragment>标签中对应Fragment的标识,用来给其他Fragment跳转使用;name: 对应的自定义Fragment;
label: 内容包含目标Fragment的XML布局文件名称
layout:当前标签对应Fragment的布局文件
<action>标签在<fragment>标签中,其主要是给出一个id让外部操作的时候指定,destination则表示的是当前id操作要往哪里跳转,其使用id指定跳转的fragment。
app:startDestination属性指的是默认的起始位置,它是navigation标签的属性:
<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/jetpack_navigation"
app:startDestination="@id/fragment_a">
在Activity文件中建立NavHostFragment
NavHostFragment是一个导航界面的容器,是用来展示Navigation的一系列Fragment。看下面xml的代码:
<?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">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/jetpack_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
这里请注意id是必须要添加的,不然运行起来会奔溃
name:这里指的是NavHost实现类的名称;
navGraph:存放的是建好在Navigation中的资源文件,也就是确定了Navigation Graph;
defaultNavHost:与系统的返回按钮相关联,true代表属性可以指定NavHostFragment会拦截系统返回按钮。
跳转实现
以下代码一次是FragmentA、B、C的onViewCreated方法里面的点击事件:
//FragmentA
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv_a_b?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_a_to_b)
}
}
//FragmentB
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv_b_a?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_a)
}
tv_b_c?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_c)
}
}
//FragmentC
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tv_c_b?.setOnClickListener {
Navigation.findNavController(it).navigate(R.id.action_fragment_c_to_b)
}
}
演示效果:
使用Bundle传值
使用Bundle传值非常简单,我们再FragmentA点击跳转把值传进来:
tv_a_b?.setOnClickListener {
val bundle = Bundle()
bundle.putString("Key","我在Fragment A过来的")
Navigation.findNavController(it).navigate(R.id.action_fragment_a_to_b,bundle)
}
然后在FragmentB的onViewCreated中接收:
val value = arguments?.getString("Key")
Toast.makeText(activity, value?.saveAs<String>(), Toast.LENGTH_SHORT).show()这里比较有一点bug的就是目前navigation在每次切换后Fragment都会被销毁重建,谷歌那边说会在navigation-fragment-ktx的2.4.0版本修复,但目前依旧还是存在重建的问题。如果要实现Fragment复用请参考:
Fragment切换动画效果
加上动画的过渡效果,让Fragment切换起来没有那么生硬,更像是activity是跳转,这样也可以提高一下用户体验。我们先定义两个动画,分别是:进场动画和退出动画。
frg_in:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 定义从右向左进入的动画 --> <translate android:duration="300" android:fromXDelta="100%" android:interpolator="@android:anim/accelerate_interpolator" android:toXDelta="0" /> </set>
frg_out:
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:duration="300" android:fromXDelta="0" android:toXDelta="-100%" android:interpolator="@android:anim/accelerate_interpolator" /> </set>
Fragment之间跳转的过渡动画有两种实现方式,一种是在navigation文件中的<action>标签配置enterAnim和exitAnim属性:
<fragment android:id="@+id/fragment_a" android:name="com.transocks.myapplication.FragmentA" android:label="fragment_a" tools:layout="@layout/fragment_a" > <action android:id="@+id/action_fragment_a_to_b" app:destination="@id/fragment_b" app:enterAnim="@anim/frg_in" app:exitAnim="@anim/frg_out" /> </fragment>
另外一种方式就是通过navOptions代码实现:
val options = navOptions {
anim {
//这里换一个进入动画,容易看出区别
enter = R.anim.frg_in_bottomtoup
exit = R.anim.frg_out
}
}
tv_b_c?.setOnClickListener {
//这个null是bundle,因为没有传值就给个null了
Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_c,null,options)
}效果展示:
NavigationUI
上面介绍了Navigation管理Fragment的一般用法,这种交互的场景很适合用在诸如登录页面,可以调整注册页面、忘记密码页面,但对于我们现阶段使用的习惯,Fragment用在最多的场景无可厚非是在首页Tab切换不同页面的时候。在以往,我们可以使用Tablayout实现操作Fragment,而Navigation则可以绑定menus、drawers和bottom navigation,我们通过BottomNavigationView结合Navigation实现底部导航栏切换Fragment的功能。
首先,我们res下创建一个menu文件夹,并创建一个menu_tab的menu资源文件:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/fragment_a" android:icon="@drawable/tab_home_normal" android:title="首页" /> <item android:id="@+id/fragment_b" android:icon="@drawable/tab_account_normal" android:title="账号" /> <item android:id="@+id/fragment_c" android:icon="@drawable/tab_more_normal" android:title="更多" /> </menu>
这里的id就是jetpack_navigation文件中各<fragment>标签中的id,然后在MainActivity布局文件中添加BottomNavigationView,并关联创建的menu_tab文件,代码如下:
<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/tab_navi" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:menu="@menu/menu_tab" />
最后在MainActivity中把NavController取出来,并BottomNavigationView绑定,看下面代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//绑定BottomNavigationView
val navHost: NavHostFragment? = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment)?.saveAs<NavHostFragment>()
val navController = navHost?.navController
val bottomNaviView by bindView<BottomNavigationView>(R.id.tab_navi)
navController?.let { bottomNaviView?.setupWithNavController(it) }
}
}NavController是通过NavHostFragment获取的,所以通过supportFragmentManager.findFragmentById(R.id.nav_host_fragment)把它拿到。 效果如下:
如果要对BottomNavigationView切换进行监听可用:setOnNavigationItemSelectedListener。 另外Navigation还支持和以下控件进行绑定使用:
Toolbar
CollapsingToolbarLayout
ActionBar
DrawerLayout
代码动态加载Navigation的xml资源文件
要代码动态加载navigation首先要把MainActivity中布局文件的app:navGraph="@navigation/jetpack_navigation"去掉:
<androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
接下来要做到的就是在MainActivity中通过navController把Navigation的xml文件进行inflater,并通过navController的graph设置进来:
//绑定BottomNavigationView
val navHost: NavHostFragment? = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)?.saveAs<NavHostFragment>()
val navController = navHost?.navController
//设置graph
val navGraph = navController?.navInflater?.inflate(R.navigation.jetpack_navigation)
navController?.graph = navGraph ?: return
val bottomNaviView by bindView<BottomNavigationView>(R.id.tab_navi)
navController?.let { bottomNaviView?.setupWithNavController(it) }Navigation的返回栈
返回栈是Android维护的一个堆栈,里面包含了我们访问的每一个Destination(指的是跳转的目的地),对于activity来说,一般返回后调用finish都会将当前Activity从堆栈中退出并销毁。而对于Navigation管理的Fragment,每一次navigate()方法都会把对应的Destination放到栈顶,所以每次调用导航action时都会往回退栈中添加一个Destination。如此反复,回退栈中将会包含很多重复的Destination。当我们通过NavController 调用 navigateUp()和popBackStack() 方法来进行前进或向后的操作时,会移除栈顶的目的地。如果要对action配置进行返回栈的清空栈的操作则可以通过添加app:launchSingleTop="true"、app:popUpTo="@+id/jetpack_navigation"和app:popUpToInclusive="true"属性来设置:
<fragment
android:id="@+id/fragment_b"
android:name="com.transocks.myapplication.FragmentB"
android:label="fragment_b"
tools:layout="@layout/fragment_b" >
<action
android:id="@+id/action_fragment_b_to_c"
app:destination="@id/fragment_c" />
<action
android:id="@+id/action_fragment_b_to_a"
app:destination="@id/fragment_a"
app:launchSingleTop="true"
app:popUpTo="@+id/jetpack_navigation"
app:popUpToInclusive="true"/>
</fragment>
这里设置就是当我们返回第一个Fragment时,先清空返回栈再跳转。
在代码中我们可以这样实现:
tv_b_a?.setOnClickListener {
val navOption = NavOptions.Builder()
.setLaunchSingleTop(true)
.setPopUpTo(R.id.jetpack_navigation, true)
.build()
Navigation.findNavController(it).navigate(R.id.action_fragment_b_to_a,null,navOption)
}
如果要在activity中清空返回栈,则可以通过调用popBackStack方法:
navController.popBackStack(R.id.jetpack_navigation,true)
小结
Navigation组件一个用来构建应用界面的框架,它解决了以往通过FragmentTransaction管理Fragment的麻烦。当然,谷歌推出此组件最重要的是想推广一个Activity架构开发应用的想法。本文只是简单介绍了Navigation