上一篇:Android多回退栈实践(一) - 掘金 (juejin.cn)
在上一篇文章中,我们介绍了Android中的多回退栈,并使用FragmentManager
实现了最朴素的多回退栈用例。接下来,我们将借助Android的Navigation组件,更加方便的实现多回退栈。
已知我们已经有6个页面:
Music,Favorite,Collection,MusicDetail,FavoriteDetail,CollectionDetail。
引入Jetpack Navigation之后,我们构建一个Graph:
<?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"
android:id="@+id/nav_multi_stack"
app:startDestination="@id/music">
<navigation
android:id="@+id/music"
app:startDestination="@+id/music_main">
<fragment
android:id="@+id/music_main"
android:name="xxx.MusicFragment"
android:label="MusicFragment">
<action
android:id="@+id/action_music_to_detail"
app:destination="@id/music_detail" />
</fragment>
<fragment
android:id="@+id/music_detail"
android:name="xxx.MusicDetailFragment"
android:label="MusicDetailFragment" />
</navigation>
<navigation
android:id="@+id/favorite"
app:startDestination="@+id/favorite_main">
<fragment
android:id="@+id/favorite_main"
android:name="xxx.FavoriteFragment"
android:label="FavoriteFragment">
<action
android:id="@+id/action_favorite_to_detail"
app:destination="@id/favorite_detail" />
</fragment>
<fragment
android:id="@+id/favorite_detail"
android:name="xxx.FavoriteDetailFragment"
android:label="FavoriteDetailFragment" />
</navigation>
<navigation
android:id="@+id/collection"
app:startDestination="@+id/collection_main">
<fragment
android:id="@+id/collection_main"
android:name="xxx.CollectionFragment"
android:label="CollectionFragment">
<action
android:id="@+id/action_collection_to_detail"
app:destination="@id/collection_detail" />
</fragment>
<fragment
android:id="@+id/collection_detail"
android:name="xxx.CollectionDetailFragment"
android:label="CollectionDetailFragment" />
</navigation>
</navigation>
我们仔细看下这里面的细节,我们构建的一个大的Graph中,其实包含了三个嵌套的Graph,里面分别有不同的跳转Action。
Music->MusicDetail,Favorite->FavoriteDetail,Collection->CollectionDetail。
我们修改一下Music->MusicDetail的跳转代码,剩余两个同理,也需要修改:
root.thing.setOnClickListener {
findNavController().navigate(R.id.action_music_to_detail)
}
此时,我们的主Fragment
跳转到详情Fragment
的Action就已经使用Navigation套件的跳转方法了。
现在,我们来配置主页。
主页的xml布局:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0px"
android:layout_height="0px"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/bottom_nav"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_multi_stack" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
style="@style/Widget.MaterialComponents.BottomNavigationView.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/bottom_navigation_multistack" />
</androidx.constraintlayout.widget.ConstraintLayout>
更改后的主页代码:
class MultiStackPage : AppCompatActivity() {
private var mSelectId = -1
private val controller by lazy { (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_multi_stack)
findViewById<BottomNavigationView>(R.id.bottom_nav).let { nav ->
nav.setOnItemSelectedListener {
if (mSelectId == it.itemId) {
return@setOnItemSelectedListener true
}
when (it.itemId) {
R.id.action_music -> {
controller.navigate(
R.id.music, null, NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true).setPopUpTo(
controller.graph.findStartDestination().id, inclusive = false, saveState = true
).build()
)
}
R.id.action_favorite -> {
controller.navigate(
R.id.favorite, null, NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true).setPopUpTo(
controller.graph.findStartDestination().id, inclusive = false, saveState = true
).build()
)
}
R.id.action_collection -> {
controller.navigate(
R.id.collection, null, NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true).setPopUpTo(
controller.graph.findStartDestination().id, inclusive = false, saveState = true
).build()
)
}
}
mSelectId = it.itemId
true
}
nav.selectedItemId = R.id.action_music
}
}
}
其实非常简单,我们着重看一个跳转是如何处理的:
controller.navigate(
R.id.music, null, NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true).setPopUpTo(
controller.graph.findStartDestination().id, inclusive = false, saveState = true
).build()
)
在点击BottomNavigationView
的item
的时候,我们使用NavControler.navigate
进行跳转,第一个参数R.id.music
代表我们要跳转的嵌套图,我们现在有三个嵌套图,点击第一个Music的时候,跳转到Music的嵌套图,当然,我们还需要配置后面的NavOptions
:
setLaunchSingleTop
如果为true
,表示如果我们重新回到当前的item
,该item
放置在顶部,同时也可以避免多份拷贝。setRestoreState
如其函数名,表示恢复保存的状态到回退栈。setPopUpTo
,表示当前回退栈退栈,同时saveState = true
保存当前弹出的操作。 我们在上一篇文章中,已经知道了restoreState
,以及saveState
的用法,这里不再赘述。
配置完成NavOptions
之后,我们就可以进行正常的跳转了。
此时,我们就已经完成借助Navigation
实现了多回退栈。
可能大部分读者会认为,这也太麻烦了。有没有更简单一点的方法实现?当然有,Android的Jetpack Navigation组件中,提供了一个androidx.navigation:navigation-ui-ktx
的依赖,我们引入这个依赖,能更快速的实现BottomNavigationView
与Fragment
的多回退栈策略。步骤如下:
- 更改
Menu
文件的实现,id
需要和Navigation的Graph中图的id
保持一致。<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/music" android:enabled="true" android:icon="@drawable/outline_music_note_24" android:title="Music" /> <item android:id="@+id/favorite" android:enabled="true" android:icon="@drawable/outline_favorite_24" android:title="Favorite" /> <item android:id="@+id/collection" android:enabled="true" android:icon="@drawable/outline_collections_24" android:title="Collection" /> </menu>
- 修改主页的代码,只需要一步:
findViewById<BottomNavigationView>(R.id.bottom_nav).let { nav -> nav.setupWithNavController(controller) }
完毕。
此时,我们就借助NavigationUI实现了多回退栈,很方便,不是么?我们不需要编写额外的代码,只需要在定义Menu
时注意Menu Item
的id
即可,NavigationUI早已为我们准备好了一切。
以上,我们便完成了关于Android多回退栈的所有实践方法,总结一下,有如下三种方法可供选择:
- 使用
FragmentManager
手动控制回退栈,重点接口是saveBackStack
,restoreBackStack
。 - 借助于Navigation,使用
NavControler.navigate
控制回退栈。 - 借助于NavigationUI,解放双手,无需做额外的管理。
在具体项目中,我们更推荐后两种方法,简单,高效。
祝各位好运!