Android多回退栈实践(二)

1,472 阅读3分钟

上一篇:Android多回退栈实践(一) - 掘金 (juejin.cn)

在上一篇文章中,我们介绍了Android中的多回退栈,并使用FragmentManager实现了最朴素的多回退栈用例。接下来,我们将借助AndroidNavigation组件,更加方便的实现多回退栈。
已知我们已经有6个页面:
MusicFavoriteCollectionMusicDetailFavoriteDetailCollectionDetail
引入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->MusicDetailFavorite->FavoriteDetailCollection->CollectionDetail
我们修改一下Music->MusicDetail的跳转代码,剩余两个同理,也需要修改:

root.thing.setOnClickListener {
    findNavController().navigate(R.id.action_music_to_detail)
}

此时,我们的主Fragment跳转到详情FragmentAction就已经使用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()
)

在点击BottomNavigationViewitem的时候,我们使用NavControler.navigate进行跳转,第一个参数R.id.music代表我们要跳转的嵌套图,我们现在有三个嵌套图,点击第一个Music的时候,跳转到Music的嵌套图,当然,我们还需要配置后面的NavOptions

  • setLaunchSingleTop如果为true,表示如果我们重新回到当前的item,该item放置在顶部,同时也可以避免多份拷贝。
  • setRestoreState如其函数名,表示恢复保存的状态到回退栈。
  • setPopUpTo,表示当前回退栈退栈,同时saveState = true保存当前弹出的操作。 我们在上一篇文章中,已经知道了restoreState,以及saveState的用法,这里不再赘述。
    配置完成NavOptions之后,我们就可以进行正常的跳转了。

此时,我们就已经完成借助Navigation实现了多回退栈。

可能大部分读者会认为,这也太麻烦了。有没有更简单一点的方法实现?当然有,AndroidJetpack Navigation组件中,提供了一个androidx.navigation:navigation-ui-ktx的依赖,我们引入这个依赖,能更快速的实现BottomNavigationViewFragment的多回退栈策略。步骤如下:

  1. 更改Menu文件的实现,id需要和NavigationGraph中图的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>
    
  2. 修改主页的代码,只需要一步:
    findViewById<BottomNavigationView>(R.id.bottom_nav).let { nav ->
        nav.setupWithNavController(controller)
    }
    

完毕。
此时,我们就借助NavigationUI实现了多回退栈,很方便,不是么?我们不需要编写额外的代码,只需要在定义Menu时注意Menu Itemid即可,NavigationUI早已为我们准备好了一切。

以上,我们便完成了关于Android多回退栈的所有实践方法,总结一下,有如下三种方法可供选择:

  • 使用FragmentManager手动控制回退栈,重点接口是saveBackStackrestoreBackStack
  • 借助于Navigation,使用NavControler.navigate控制回退栈。
  • 借助于NavigationUI,解放双手,无需做额外的管理。

在具体项目中,我们更推荐后两种方法,简单,高效。

祝各位好运!