经验之谈-Fragment中监听返回键

常见的方案:

方案一

activity中包含fragment实例的引用,在activity中的onBackPressed()中调用fragment的方法,传递返回键事件。

缺点:增加了activity和fragment之间的耦合性;当有子fragment时,一级级的向下传递也稍显麻烦。

方案二

Google官方提供的onBackPressedDispatcher,该类支持直接在fragment中监听返回键事件。

缺点:

  1. 需要使用isEnable()方法来手动管理对返回键的监听,稍显麻烦.

  2. 每一个fragment都需要监听返回键逻辑,然而监听的顺序决定了,返回键事件的分发顺序,所以事件的分发顺序得不到保证。

  3. fragment再订阅后,即使页面不可见了,也能收到返回键事件。

理想中的方案:

  1. activity和fragment,fragment之间拒绝耦合。
  2. 返回键事件的分发顺序,应该让子fragment先处理,如果不处理再传递给父fragment处理,类似View触摸事件的分发顺序。
  3. 当fragment不可见时,不应该接受到返回键事件。
  4. 交互上要和activity的onBackPressed()类似,并且通过返回值表示要不要消费该事件。

那么要实现上述效果,有两种可行的封装方案:

方案一(继承):

在BaseFragemnt中使用onBackPressedDispatcher监听返回键,然后留出 onFragmentBackPressed()方法给子类实现,通过返回值判断是否消费事件。同时控制不可见的fragment不分发事件。

使用时只需要继承BaseFragemnt类,实现onFragmentBackPressed()就可以。

上面的封装基本能解决需求啦,不过由于使用的是继承,所以也有一些不太完美的地方 比如:

  1. 需要对原有项目的BaseFragemnt做较大的改动,如果有多个BaseFragemnt都要同步修改;

  2. 项目中需要处理返回键的fragment属于少数,所以也就让其他Fragment被迫继承了它们用不着的功能。

所以就有了一个经典的设计原则:多用组合,少用继承

那么再尝试使用组合方式进行封装,把逻辑都封装到一个新类中,哪里需要哪里引用,更为灵活。

方案二(组合):

主要逻辑步骤:

  1. 获取和监听activity中所有的fragment变动。
  2. 把获得的fragment列表转化为fragment树(根据fragment间的父子关系)。
  3. 从树的顶节点(activity)开始向可见的fragment分发返回键事件。

先看下怎么使用

kotlin 复制代码
//MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_backpress_test)
    BackPressDispatcher(this).init() //再activity中调用初始化方法
}
kotlin 复制代码
//再需要处理返回键的fragment中继承接口FragmentBackPressListener,实现方法onFragmentBackPressed
class BackPressTestFragment01: Fragment(), FragmentBackPressListener {

    override fun onFragmentBackPressed(): Boolean {
        Log.e(TAG, "${TAG} 收到了返回键事件")
        return false //返回值代表是否消费事件
    }
}

使用起来挺简单吧!

代码实现也很简单 如下:

kotlin 复制代码
import android.content.Context
import android.util.Log
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager


class BackPressDispatcher(val hostActivity: FragmentActivity) {
    private val TAG = "BackPressDispatcher"
    private val rootPageNode = PageNode(null)
    private val fragmentPageNodeMap = hashMapOf<Fragment, PageNode>()

    fun init(){
        val fragmentManager = hostActivity.supportFragmentManager
        transferFragments2PageNodeTree(getAllFragments(fragmentManager))
        fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
        hostActivity.onBackPressedDispatcher.addCallback(hostActivity, backStackChangeListener)
    }

    private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
        override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
            super.onFragmentAttached(fm, f, context)
            Log.e(TAG, "onFragmentAttached")
            transferFragments2PageNodeTree(arrayListOf(f))
        }

        override fun onFragmentDetached(fm: FragmentManager, f: Fragment) {
            super.onFragmentDetached(fm, f)
            removeFromPageNodeTree(f)
        }
    }

    private val backStackChangeListener = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if(!dispatchBackPressEvent(rootPageNode)){
                isEnabled = false
                hostActivity.onBackPressed()
                isEnabled = true
            }
        }
    }

    private fun dispatchBackPressEvent(pageNode: PageNode): Boolean{
        //不可见的fragment不参与事件分发
        if(pageNode.currFragment?.isResumed == false) return false

        //先派发给子fragment处理
        pageNode.getChildFragments().forEach {
            fragmentPageNodeMap[it]?.let { childPageNode ->
                if(dispatchBackPressEvent(childPageNode)) return true
            }
        }

        //再派发给当前fragment处理
        return (pageNode.currFragment as? FragmentBackPressListener)?.onFragmentBackPressed()?:false
    }


    private fun transferFragments2PageNodeTree(fragments: List<Fragment>){
        Log.e(TAG, "fragments size: ${fragments.size}")
        if(fragments.isEmpty()) return
        for (item in fragments){
            val currPageNode = fragmentPageNodeMap[item]?:PageNode(item)

            item.parentFragment?.let { parentFragment ->
                val parentPageNode = fragmentPageNodeMap[parentFragment]?:PageNode(parentFragment)
                parentPageNode.addChildFragment(item)
                fragmentPageNodeMap.put(parentFragment, parentPageNode)
            }?: run { //没有父Fragment,说明上级就是activity啦
                rootPageNode.addChildFragment(item)
            }

            fragmentPageNodeMap.put(item, currPageNode)
        }

    }


    private fun removeFromPageNodeTree(fragment: Fragment){
        fragment.parentFragment?.let { parentFragment ->
            fragmentPageNodeMap[parentFragment]?.removeChildFragment(fragment)
        }?: run{//没有父Fragment,说明上级就是activity啦
            rootPageNode.removeChildFragment(fragment)
        }
        fragmentPageNodeMap.remove(fragment)
    }

    private fun getAllFragments(fm: FragmentManager): List<Fragment> {
        val result = mutableListOf<Fragment>()
        for (fragment in fm.fragments) {
            result.add(fragment)
            result.addAll(getAllFragments(fragment.childFragmentManager))
        }
        return result
    }

}
kotlin 复制代码
import androidx.fragment.app.Fragment

//一个fragment对应一个这样的节点;activity节点的currFragment == null
class PageNode(var currFragment: Fragment?) {
    private var childFragments = LinkedHashSet<Fragment>()


    fun addChildFragment(fragment: Fragment){
        childFragments.add(fragment)
    }

    fun removeChildFragment(fragment: Fragment){
        childFragments.remove(fragment)
    }

    fun getChildFragments(): LinkedHashSet<Fragment>{
        return childFragments
    }

}
kotlin 复制代码
interface FragmentBackPressListener {
    fun onFragmentBackPressed(): Boolean
}

完整项目地址:github.com/High-Power-...

注意:上述代码未经过充分测试

相关推荐
儿歌八万首18 分钟前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-19433 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
Jinkxs3 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&3 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin
LDORntKQH3 小时前
基于深度强化学习的混合动力汽车能量管理策略 1.利用DQN算法控制电池和发动机发电机组的功率分配 2
android
冬奇Lab3 小时前
Android 15 ServiceManager与Binder服务注册深度解析
android·源码·源码阅读
2501_916008895 小时前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
独行soc6 小时前
2026年渗透测试面试题总结-20(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
常利兵7 小时前
2026年,Android开发已死?不,它正迎来黄金时代!
android
Risehuxyc7 小时前
备份三个PHP程序
android·开发语言·php