Fragment监听返回键的一种合理方式

10,557 阅读11分钟

原创不易,转载请注明出处; juejin.im/post/689064…

2023-03-07更新

建议实现方式:扩展两个方法,直接调用这两个方法即可,这种方式综合考虑下来没有引入额外的定义,都是对官方组件的直接使用。此方法只是将后文中第五种方法改为扩展方法而已

/**
 * 绑定返回键回调(建议使用该方法)
 * @param owner Receive callbacks to a new OnBackPressedCallback when the given LifecycleOwner is at least started.
 * This will automatically call addCallback(OnBackPressedCallback) and remove the callback as the lifecycle state changes. As a corollary, if your lifecycle is already at least started, calling this method will result in an immediate call to addCallback(OnBackPressedCallback).
 * When the LifecycleOwner is destroyed, it will automatically be removed from the list of callbacks. The only time you would need to manually call OnBackPressedCallback.remove() is if you'd like to remove the callback prior to destruction of the associated lifecycle.
 * @param onBackPressed 回调方法;返回true则表示消耗了按键事件,事件不会继续往下传递,相反返回false则表示没有消耗,事件继续往下传递
 * @return 注册的回调对象,如果想要移除注册的回调,直接通过调用[OnBackPressedCallback.remove]方法即可。
 */
fun androidx.activity.ComponentActivity.addOnBackPressed(
    owner: LifecycleOwner,
    onBackPressed: () -> Boolean
): OnBackPressedCallback {
    return backPressedCallback(onBackPressed).also {
        onBackPressedDispatcher.addCallback(owner, it)
    }
}

/**
 * 绑定返回键回调,未关联生命周期,建议使用关联生命周期的办法(尤其在fragment中使用,应该关联fragment的生命周期)
 */
fun androidx.activity.ComponentActivity.addOnBackPressed(onBackPressed: () -> Boolean): OnBackPressedCallback {
    return backPressedCallback(onBackPressed).also {
        onBackPressedDispatcher.addCallback(it)
    }
}

private fun androidx.activity.ComponentActivity.backPressedCallback(onBackPressed: () -> Boolean):OnBackPressedCallback{
    return object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (!onBackPressed()) {
                isEnabled = false
                onBackPressedDispatcher.onBackPressed()
                isEnabled = true
            }
        }
    }
}

使用示例:

//fragment中使用
val callback = requireActivity().addOnBackPressed {
    // do your biz
    true//返回true表示消耗了事件,false则表示未消耗,事件继续往下传递
}

//需要手动移除注册的情况下
callback.remove()

//在其他场景下要注册:比如View或者Activity中
//首先view获取的context,或者activity必须是FragmentActivity(一般是这个类型),获取activity之后调用addOnBackPressed方法即可

//java代码调用:其实也差不多,把上面的kt扩展方法放入kt文件,然后通过文件名+kt进行方法访问即可(比如放在A文件中,则Akt.addOnBackPressed即可调用:Akt是默认名字,其实也可以改文件名字)

开场

以下场景为杜撰:

产品经理:“小罗,这个信息发送界面,如果用户输入了内容,点击返回键的时候,要先询问用户是否保存草稿箱哈”。

小罗:“收到,这问题简单。”

说完小罗就准备着手处理,然后却发现信息编辑界面是一个Fragment,然而Fragment并没有提供返回键点击的直接处理;小罗虽菜,但是摸鱼也摸了些年头了,这问题难不倒小罗。

小罗心想,反正Activity提供了onBackPressed方法,再不济的情况把这个操作分发到Fragment中去就好,可是对于处女座的小罗来说,在解决问题的基础上,起码代码要写的漂亮一点,写的漂亮一点心里就舒服一点,心里舒服一点就...(此处内容很长)。

小罗坚信“条条大路通罗马”,我们不仅要到罗马,还要风风光光的去,所以对于“Fragment如何监听返回键的点击”,小罗决定下点功夫;

为什么关注的点是Fragment去监听返回键,而不是其他?其实在现在的开发过程中,Fragment的使用比重是非常大的,对于个人而言,几乎整个工程的界面实现都是基于Fragment而非Activity。

一、最lowB的方式(不推荐)

这就是小罗心里的预备方案,在实在没有办法的时候会采用此方法,也就是前面提到的,我们可以在Activity执行onBackPressed时,分发到Fragment中去;那我们用什么来分发呢?这个分发就好比是连接ActivityFragment之间的一个纽带,双方均能够访问到这个对象就可以了,所以一个可以的选择之一是使用ViewModel,当然还可以有其他选择,在此就不细聊了。

二、使用OnKeyListener(不推荐)

这种方式可能不常用,不容易想到这方面,所以这种方式也不推荐,简单做个了解;

通过设置ViewOnKeyListener来监听返回键的处理,此方法也没什么大的弊端,只是要注意以下两点:

1、如果把这个功能封装在Fragment基类中的话,可能存在被覆盖的问题;比如在基类中设置了OnKeyListener,而子类也需要设置OnKeyListener,此时设置的监听则会替换默认设置的监听,从而导致意想不到的可能,不过此问题几乎不太可能发生。

2、需要注意这种方式将会改变返回键处理的顺序,也就是会先处理OnKeyListener的回调,再处理ActivityonBackPressed,所以要注意这个关系。

三、Jetpack提供的方式

其实对于返回键的分发,官方已经做了支持,在Activity中提供了一个用于分发返回键事件的对象,通过调用ActivitygetOnBackPressedDispatcher()方法得到这个对象,由于这个对象是在比较底层的androidx.activity.ComponentActivity中提供的(AppCompatActivity->FragmentAcitivty->androidx.activity.ComponentActivity),所以在Fragment中可以直接拿到这个对象添加回调;

官方资料入口

//官方使用示例 
public class FormEntryFragment extends Fragment {
    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        //定义回调
        OnBackPressedCallback callback = new OnBackPressedCallback(
            true // default to enabled
        ) {
            @Override
            public void handleOnBackPressed() {
                showAreYouSureDialog();
            }
        };
      
       //获取Activity的返回键分发器添加回调
      requireActivity().getOnBackPressedDispatcher().addCallback(
            this, // LifecycleOwner
            callback);
    }
}

简单明了,这个事情好像到此为止了~~ 但随着深入了解,事情似乎没有这么简单,经过源码分析和资料收集,发现如果直接使用会存在以下弊端:

1、Fragment回调处理时,无法向上传递 2、回调是否可用需要主动标记,而非运行时确定

简单说一下OnBackPressedDispatcher分发返回键的流程:

	//官方源码
    @MainThread
    public void onBackPressed() {
        Iterator<OnBackPressedCallback> iterator =
                mOnBackPressedCallbacks.descendingIterator();
        while (iterator.hasNext()) {
            OnBackPressedCallback callback = iterator.next();
            if (callback.isEnabled()) {
                callback.handleOnBackPressed();
                return;
            }
        }
        if (mFallbackOnBackPressed != null) {
            mFallbackOnBackPressed.run();
        }
    }

当分发返回键事件时,会倒序循环遍历已经注册的回调,如果回调isEnabled设置为true,则执行回调的方法,分发结束;

那前面提到的弊端是怎么产生的呢?假如一个Activity有两个Fragment AB,均注册了返回键点击事件(有童鞋会说了,这种场景不太可能存在,确实,这种场景是不多,但不代表没有,做一些了解也不是坏事),并且两个回调的isEnabled均设置为true,那么当分发事件时,会将事件分发给B,但是B此时并不需要处理返回键事件,但是B又没有办法再继续将事件传递给A了;

“你傻啊,你B不执行返回键事件,就设置isEnablefalse啊”

“是啊,B不执行事件是该设置为false,可是我怎么知道什么时候去把它设置成false?难道动态绑定判断条件的值进行设置么?”

转头一想“咦,好像确实可以动态修改回调的isEnabled值呢,将回调的值跟一个LiveData绑定不就可以了么!”

理是这个理,但是我不愿意做额外的工作,我不愿这么干,谁知道动态判断条件到底有多复杂呢,难道我不可以在返回键点击的时候去判断么?

四、灵机一动,官方升级版(推荐方式)

官方的方式不是存在上面两个弊端么,解决这两个问题不就好了;所以结合官方OnBackPressedDispatcherOnKeyListener两者的优点,创建了andme.arch.activity.AMBackPressedDispatcher,在保留官方原有的功能的同时,更改事件分发流程,并将返回键持有者一并传入,用于解决一些更复杂一点的需求;

  @MainThread
  fun onBackPressed(): Boolean {
      if (!hasRegisteredCallbacks())
          return false

      val iterator = mOnBackPressedCallbacks.descendingIterator()
      while (iterator.hasNext()) {
          val callback = iterator.next()
          //判断回调是否需要消耗事件在决定是否继续传递
          if (callback.handleOnBackPressed(owner)) {
              return true
          }
      }
      return false
  }

五、官方使用技巧版

这种方法其实是我在发布文章之后,群友提供的一种思路,说实话,非常有技巧,刚开始看到的时候眼前一亮;其核心原理是默认注册的回调是可用的,在回调执行中,先判断自己是否需要执行回调,如果不需要执行回调,则将自己的isEnabled设置为false,然后再调用OnBackPressedDispatcher重新分发返回键事件(由于此时已将自己设置为false,此时便不会响应回调),调用方法之后再将isEnabled设置为true,巧用了递归,该方式不错的;

最开始群友提供的代码有一丢丢瑕疵,以下为修正之后的代码,在Fragment中定义这两个方法,在需要绑定返回键监听的时候调用这个两个方法之一即可(推荐调用与生命周期相关的方法);

fun addOnBackPressed(onBackPressed: () -> Boolean): OnBackPressedCallback {
        val callback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                if (!onBackPressed()) {
                    isEnabled = false
                    requireActivity().onBackPressedDispatcher.onBackPressed()
                    isEnabled = true
                }
            }
        }
        requireActivity().onBackPressedDispatcher.addCallback(callback)
        return callback
    }

    fun addOnBackPressed(owner: LifecycleOwner, onBackPressed: () -> Boolean): OnBackPressedCallback {
        val callback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                if (!onBackPressed()) {
                    isEnabled = false
                    requireActivity().onBackPressedDispatcher.onBackPressed()
                    isEnabled = true
                }
            }
        }
        requireActivity().onBackPressedDispatcher.addCallback(owner,callback)
        return callback
    }

但是经过慎重思考,最终我还是没有用这种方法,虽然这种方法在几乎百分之八九十的情况下是没有问题的,但是我认为可能还是有场景无法满足;

举个例子,一个Activity添加了一个Fragment,这个Fragment又顺序添加了AB两个ChildFragment,那在B执行返回处理的时候,是想回到A还是finish呢?或者是其他呢,也是就是说我们无法确定,在Fragment执行返回键处理时,是否需要直接调用Activity.super.onBackPressed方法的可能。

我们永远无法预估用户的场景到底有多复杂,需求有多变态,所以尽可能的考虑把。

总结

综上所述,我目前还是会继续使用第四种我写的方案,第五种方案也推荐,毕竟在绝大部分场景中都是没有问题的

那么我们考虑第四种方案到底是否可行? 1、功能性 满足了功能需求,并且至少目前是没有想到有任何可能出现问题的场景 2、侵入性 几乎对用户场景没什么影响吧,只是对用户提供了一个可见的处理返回键事件的方法而已 3、替换性 如果采用第四种方案,要更换成第五种方案,容易么?一两句代码的事情而已 或者更换成其他方案容易么?也是一两句代码的的事情而已 并且即便替换成其他方案,也不会对现有系统造成任何影响,因为对于Fragment监听返回键这个需求来讲,这个需求的核心就是需要一个在Fragment中处理返回键事件的方法而已,其他东西对用户来讲都是无感的

所以总体觉得没什么毛病; 如果你有更好的思路,欢迎沟通,不胜感激;

另外,上述功能其实并不仅仅支持在Fragment中处理返回键事件,理论上来说任何想要监听返回键处理的都可以通过Activity获取AMBackPressedDispatcher对象添加回调即可。

2020-12-22更新

从评论中看到另一种实现方式,通过Activity中的FragmentManager,从而获取到相关的Fragments,然后将返回键分发下去; 链接奉上

这种方式也很不错,其实说实在,更符合我这篇文章的标题,其重点在于“Fragment”处理返回键。而且实现得体,感觉思路不错。

不过本文的提供的方法在于如何分发返回键事件出去,也就是接受的对象是Fragment还是其他类型的东西,其实是没有限定的,正如我前面所说,在一个View中,在一个对象中,都可以拦截返回键事件的处理,所以感觉更灵活一点点。而且返回键事件的分发可以绑定Activity的生命周期,这一点也是很不错的。

两者使用的难易程度和代码量上,对使用者来说应该是差不多的,本文的需要调用add方法添加返回键拦截处理,而此方法需要默认实现一个接口来处理,两者均需要实现一个方法,这个就看个人习惯了。

Andme Github地址

欢迎入群交流:QQ276097690

更欢迎关注公众号:赶快扫码加入吧

如果您有更多的建议或者交流,欢迎入群讨论,添加公众号更能第一时间了解最新内容。