Android多回退栈实践(一)

1,982 阅读7分钟

Android应用中,我们可以通过点击设备实体返回按键,或者应用左上角标题栏返回按钮进行返回。

点击系统按钮返回
device-2022-04-29-153234 00_00_00-00_00_30.gif
点击应用左上角返回按钮
device-2022-04-29-153250 00_00_00-00_00_30.gif
从用户角度来讲,返回操作是一个类似栈的操作。点击返回时,之前的一系列界面,按照退栈的形式,依次退回。
从开发角度来讲,这样一系列的回退操作,称之为回退栈。

在还未出现Fragment的早期应用,我们一般是不需要关心回退栈的。用户使用如何进入,就如何退出。
在使用Fragment之后,事情开始变得麻烦起来,开发者大多数采用了非常流程的单Activity、多Fragment的UI形式,而随着产品设计的不断迭代,原始的回退策略,已经不能满足我们的要求的,期间存在很长一段时间,我们开发者自己管理回退栈逻辑,在应用层写入自己的Fragment切换逻辑,自己监听回退事件,切换到适当的Fragment。但是这样管理,非常容易出现bug,Fragment的生命周期,也变得混乱起来。这样做也不能完全逃避Android自带的回退栈管理方式,只不过从UI效果上来讲,还能满足需求。

直到2018年5月8日,谷歌正式发布了Navigation,开发者似乎从回退栈中解放了出来,它把FragmentActivity以及Dialog都当做Navigation的一个目的地,通过一种单向图的形式串联起来,使开发者可以在不同的目的地之间进行跳转。开发者不需要关心回退栈的具体细节,我们只需要设定好NavigationGraph即可,当我们需要跳转的时候,我们使用Graph中的某个ActionNavigation会帮我们准备好一切并完成跳转。

一切看起来都是那么完美,但是,如果我们想有多个回退栈呢?

Screenshot_20220429_160637.png

我们现在有这样一个用例,我们的应用首页底部有一个导航栏,这三个按钮可以进入三个不同的分类中,分别是MusicFavoriteCollection。我们希望这三个不同的分类中,它们的回退逻辑是单独的:假设从Music进入到MusicDetail,当我们切换到Favorite,然后再切回来的时候,界面显示的是MusicDetail

device-2022-04-29-161838 00_00_00-00_00_30.gif

在官方库androidx.fragment:fragment-ktx:1.4.0-alpha01中,正式支持了多回退栈的特性。
我们来看下,关于多回退栈,官方是如何支持的。

fragment-ktx库中,添加了两个关键的API:

public void saveBackStack(@NonNull String name)
public void restoreBackStack(@NonNull String name)

我们先来看saveBackStack的解释:

Save the back stack. While this functions similarly to popBackStack(String, int), it does not throw away the state of any fragments that were added through those transactions. Instead, the back stack that is saved by this method can later be restored with its state in tact.
This function is asynchronous -- it enqueues the request to pop, but the action will not be performed until the application returns to its event loop.

参照文档,这个函数的作用有点类似popBackStack函数,但是saveBackStack并不会完全像popBackStack那样(popBackStack会退栈,同时事务里面的状态会丢掉,回退到的Fragment会执行状态恢复),saveBackStack也会执行退栈,但是他会把退栈的这些事务保存起来,用于后面的函数restoreBackStack执行。

简单解释下:

假设现在添加三个Fragment进去:

supportFragmentManager.commit {
    setReorderingAllowed(true)
    replace<AFragment>(R.id.container)
}

supportFragmentManager.commit {
    setReorderingAllowed(true)
    replace<BFragment>(R.id.container)
    addToBackStack("B")
}

supportFragmentManager.commit {
    setReorderingAllowed(true)
    replace<CFragment>(R.id.container)
    addToBackStack("C")
}

Snipaste_2022-04-29_17-23-11.png

现在,当我调用saveBackStack时:

supportFragmentManager.saveBackStack("B")

Snipaste_2022-04-29_18-15-31.png

FragmentManager会将当前保存的回退栈,存入到一个MapBackStackState中,同时我们回退栈栈顶就变成了A

于是,此时如果我们再次进行进栈操作的话:

supportFragmentManager.commit {
    setReorderingAllowed(true)
    replace<DFragment>(R.id.container)
    addToBackStack("D")
}

supportFragmentManager.commit {
    setReorderingAllowed(true)
    replace<FFragment>(R.id.container)
    addToBackStack("F")
}

Snipaste_2022-04-29_18-24-47.png

它会在当前回退栈的基础上进栈,不影响BackStackState表。

当然,我们依然可以在此时继续调用saveBackStack

supportFragmentManager.saveBackStack("D")

此时,FragmentManger所管理的栈,就会变成:

Snipaste_2022-04-29_18-30-43.png

BackStackState表中,会有两个保存的栈,一个是B,一个是D
这里在使用saveBackStack函数的时候,有一个需要注意的地方:

  • saveBackStack函数传入的参数,是保存栈的名称,该名必须是通过addToBackStack添加进去的一个已有的栈名,同时在保存到BackStackState表时,会使用该参数作为保存栈的Key

如图所示,BackStackState表可以存入多个保存栈。

现在,当我们使用restoreBackStack时,FragmentManger会帮助我们做栈恢复的操作:

supportFragmentManager.restoreBackStack("B")

Snipaste_2022-04-29_18-41-49.png

Excellent!
我们可以将已经保存的栈,恢复到当前的回退栈。这样当用户进行回退操作的时候,便会按照C->B->A的顺序,进行回退了。

回到我们提到的需求,我们现在有六个页面。
MusicFavoriteCollectionMusicDetailFavoriteDetailCollectionDetail
我们希望Music->MusicDetailFavorite->FavoriteDetailCollection->CollectionDetail,分别占用三个不互相影响的栈。具体思路就是:在切换菜单项的时候,先保存上一次的栈,然后恢复当前栈。代码会在文章最后给出,仅供参考。

借助于fragment-ktx库,我们可以通过多回退栈来实现复杂的产品需求,让开发者从回退栈的具体事务中抽出身来,更加专注业务代码。

当然,这还不是完全体,下一篇文章,我们将借助于Navigation,更快速的实现该用例。

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

完整代码:

class MultiStackPage : AppCompatActivity() {


    private var mSelectId = -1

    private var musicSaved = false
    private var favoriteSaved = false
    private var collectSaved = false

    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 -> {
                        if (mSelectId != -1) {
                            when (mSelectId) {
                                R.id.action_favorite -> {
                                    supportFragmentManager.saveBackStack(STACK_FAVORITE)
                                }

                                R.id.action_collection -> {
                                    supportFragmentManager.saveBackStack(STACK_COLLECTION)
                                }
                            }
                        }

                        if (!musicSaved) {
                            supportFragmentManager.commit {
                                setReorderingAllowed(true)
                                replace<MusicFragment>(R.id.container)
                                addToBackStack(STACK_MUSIC)
                            }
                            musicSaved = true
                        } else {
                            supportFragmentManager.restoreBackStack(STACK_MUSIC)
                        }
                    }
                    R.id.action_favorite -> {
                        if (mSelectId != -1) {
                            when (mSelectId) {
                                R.id.action_music -> {
                                    supportFragmentManager.saveBackStack(STACK_MUSIC)
                                }

                                R.id.action_collection -> {
                                    supportFragmentManager.saveBackStack(STACK_COLLECTION)
                                }
                            }
                        }

                        if (!favoriteSaved) {
                            supportFragmentManager.commit {
                                setReorderingAllowed(true)
                                replace<FavoriteFragment>(R.id.container)
                                addToBackStack(STACK_FAVORITE)
                            }
                            favoriteSaved = true
                        } else {
                            supportFragmentManager.restoreBackStack(STACK_FAVORITE)
                        }
                    }
                    R.id.action_collection -> {
                        if (mSelectId != -1) {
                            when (mSelectId) {
                                R.id.action_music -> {
                                    supportFragmentManager.saveBackStack(STACK_MUSIC)
                                }

                                R.id.action_favorite -> {
                                    supportFragmentManager.saveBackStack(STACK_FAVORITE)
                                }
                            }
                        }

                        if (!collectSaved) {
                            supportFragmentManager.commit {
                                setReorderingAllowed(true)
                                replace<CollectionFragment>(R.id.container)
                                addToBackStack(STACK_COLLECTION)
                            }
                            collectSaved = true
                        } else {
                            supportFragmentManager.restoreBackStack(STACK_COLLECTION)
                        }
                    }
                }

                mSelectId = it.itemId

                true
            }
            nav.selectedItemId = R.id.action_music
        }
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {

        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (supportFragmentManager.backStackEntryCount == 1) {
                finish()
            }
        }

        return super.onKeyDown(keyCode, event)
    }

    companion object {
        const val STACK_MUSIC = "music"
        const val STACK_FAVORITE = "favorite"
        const val STACK_COLLECTION = "collection"
    }
}

class MusicFragment : Fragment() {

    private lateinit var root: LayoutThingBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        root = LayoutThingBinding.inflate(inflater)
        return root.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        root.thing.load("https://s3.bmp.ovh/imgs/2022/04/28/a7dd490908b29ef0.jpg")
        root.thing.setOnClickListener {
            parentFragmentManager.commit {
                setReorderingAllowed(true)
                replace<MusicDetailFragment>(R.id.container)
                addToBackStack("Music_detail")
            }
        }
    }
}

class MusicDetailFragment : Fragment() {
    private lateinit var root: LayoutThingDetailBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        root = LayoutThingDetailBinding.inflate(inflater)
        return root.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        root.thing.load("https://s3.bmp.ovh/imgs/2022/04/28/a7dd490908b29ef0.jpg")
        root.detail.text =
            "Music is the art of arranging sounds in time through the elements of melody, harmony, rhythm, and timbre.[1][2] It is one of the universal cultural aspects of all human societies. General definitions of music include common elements such as pitch (which governs melody and harmony), rhythm (and its associated concepts tempo, meter, and articulation), dynamics (loudness and softness), and the sonic qualities of timbre and texture (which are sometimes termed the color of a musical sound). Different styles or types of music may emphasize, de-emphasize or omit some of these elements. Music is performed with a vast range of instruments and vocal techniques ranging from singing to rapping; there are solely instrumental pieces, solely vocal pieces (such as songs without instrumental accompaniment) and pieces that combine singing and instruments. The word derives from Greek μουσική, mousiké, '(art) of the Muses'."
    }
}

class FavoriteFragment : Fragment() {

    private lateinit var root: LayoutThingBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        root = LayoutThingBinding.inflate(inflater)
        return root.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        root.thing.load("https://s3.bmp.ovh/imgs/2022/04/28/5b3b1b7a018e2c8e.jpg")
        root.thing.setOnClickListener {
            parentFragmentManager.commit {
                setReorderingAllowed(true)
                replace<FavoriteDetailFragment>(R.id.container)
                addToBackStack("Favorite_detail")
            }
        }
    }
}

class FavoriteDetailFragment : Fragment() {
    private lateinit var root: LayoutThingDetailBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        root = LayoutThingDetailBinding.inflate(inflater)
        return root.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        root.thing.load("https://s3.bmp.ovh/imgs/2022/04/28/5b3b1b7a018e2c8e.jpg")
        root.detail.text =
            "A favourite (British English) or favorite (American English) was the intimate companion of a ruler or other important person. In post-classical and early-modern Europe, among other times and places, the term was used of individuals delegated significant political power by a ruler. It was especially a phenomenon of the 16th and 17th centuries, when government had become too complex for many hereditary rulers with no great interest in or talent for it, and political institutions were still evolving. From 1600 to 1660 there were particular successions of all-powerful minister-favourites in much of Europe, particularly in Spain, England, France and Sweden."
    }
}

class CollectionFragment : Fragment() {

    private lateinit var root: LayoutThingBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        root = LayoutThingBinding.inflate(inflater)
        return root.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        root.thing.load("https://s3.bmp.ovh/imgs/2022/04/28/182757a445a09fd0.jpg")
        root.thing.setOnClickListener {
            parentFragmentManager.commit {
                setReorderingAllowed(true)
                replace<CollectionDetailFragment>(R.id.container)
                addToBackStack("Collection_detail")
            }
        }
    }
}

class CollectionDetailFragment : Fragment() {
    private lateinit var root: LayoutThingDetailBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        root = LayoutThingDetailBinding.inflate(inflater)
        return root.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        root.thing.load("https://s3.bmp.ovh/imgs/2022/04/28/182757a445a09fd0.jpg")
        root.detail.text =
            "The Collection is the county museum and gallery for Lincolnshire in England. It is an amalgamation of the Usher Gallery and the City and County Museum. The museum part of the enterprise is housed in a new, purpose-built building close by the Usher Gallery in the city of Lincoln."
    }
}