Jetpack 系列(12)—— OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势

简介: Jetpack 系列(12)—— OnBackPressedDispatcher:Jetpack 处理回退事件的新姿势

目录

image.png


前置知识


这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~


1. 概述


  • OnBackPressedDispatcher 解决了什么问题: 在 Activity 里可以通过回调方法 onBackPressed() 处理,而 Fragment / View 却没有直接的回调方法。现在,我们可以使用 OnBackPressedDispatcher 替代 Activity#onBackPressed(),更优雅地实现回退逻辑。
  • OnBackPressedDispatcher 的整体处理流程: 分发器整体采用责任链设计模式,向分发器添加的回调对象都会成为责任链上的一个节点。当用户触发返回键时,将按顺序遍历责任链,如果回调对象是启用状态(Enabled),则会消费该回退事件,并且停止遍历。如果最后事件没有被消费,则交回到 Activity#onBackPressed() 处理。
  • OnBackPressedDispatcher 与其他方案对比: 在 OnBackPressedDispatcher 之前,我们只能通过 “取巧” 的方法处理回退事件:
  • 1、在 Fragment 中定义回调方法,从 Activity#onBackPressed() 中传递回调事件(缺点:增加了 Activity & Fragment 的耦合关系);
  • 2、在 Fragment 根布局中设置按键监听 setOnKeyListener(缺点:不灵活 & 多个 Fragment 监听冲突)。


2. OnBackPressedDispatcher 有哪些 API?


主要有以下几个,其他这几个 API 都比较好理解。其中 addCallback(LifecycleOwner, callback) 会在生命周期持有者 LifecycleOwner 进入 Lifecycle.State.STARTED 状态,才会加入分发责任链,而在 LifecycleOwner 进入 Lifecycle.State.STOP 状态时,会从分发责任链中移除。


1、添加回调对象
public void addCallback(OnBackPressedCallback onBackPressedCallback)
2、添加回调对象,关联到指定生命周期持有者
public void addCallback(LifecycleOwner owner, OnBackPressedCallback onBackPressedCallback)
3、判断是否有启用的回调
public boolean hasEnabledCallbacks()
4、回退事件分发入口
public void onBackPressed()
5、构造器(参数为最终回调)
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
    mFallbackOnBackPressed = fallbackOnBackPressed;
}
复制代码



3. OnBackPressedDispatcher 源码分析


OnBackPressedDispatcher 源码不多,我直接带着问题入手,帮你梳理 OnBackPressedDispatcher 内部的实现原理:


3.1 Activity 如何将事件分发到 OnBackPressedDispatcher?


答:ComponentActivity 内部组合了分发器对象,返回键回调 onBackPressed() 会直接分发给 OnBackPressedDispatcher#onBackPressed()。另外,Activity 本身的回退逻辑则封装为 Runnable 交给分发器处理。


androidx.activity.ComponentActivity.java


private final OnBackPressedDispatcher mOnBackPressedDispatcher =
    new OnBackPressedDispatcher(new Runnable() {
        @Override
        public void run() {
            // Activity 本身的回退逻辑
            ComponentActivity.super.onBackPressed();
        }
});
@Override
@MainThread
public void onBackPressed() {
    mOnBackPressedDispatcher.onBackPressed();
}
@NonNull
@Override
public final OnBackPressedDispatcher getOnBackPressedDispatcher() {
    return mOnBackPressedDispatcher;
}
复制代码

3.2 说一下 OnBackPressedDispatcher 的处理流程?


答:分发器整体采用责任链设计模式,向分发器添加的回调对象都会成为责任链上的一个节点。当用户触发返回键时,将按顺序遍历责任链,如果回调对象是启用状态(Enabled),则会消费该回退事件,并且停止遍历。如果最后事件没有被消费,则交回到 Activity#onBackPressed() 处理。


OnBackPressedDispatcher.java


// final 回调:Activity#onBackPressed()
@Nullable
private final Runnable mFallbackOnBackPressed;
// 责任链
final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();
// 构造器
public OnBackPressedDispatcher() {
    this(null);
}
// 构造器
public OnBackPressedDispatcher(@Nullable Runnable fallbackOnBackPressed) {
    mFallbackOnBackPressed = fallbackOnBackPressed;
}
// 判断是否有启用的回调
@MainThread
public boolean hasEnabledCallbacks() {
    Iterator<OnBackPressedCallback> iterator = mOnBackPressedCallbacks.descendingIterator();
    while (iterator.hasNext()) {
        if (iterator.next().isEnabled()) {
            return true;
        }
    }
    return false;
}
入口方法:责任链上的每个回调方法仅在前面的回调处于未启用状态(unEnabled)才能调用。
如果如果都没有启用,最后会回调给 mFallbackOnBackPressed
@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();
    }
}
复制代码

3.3 回调方法执行在主线程还是子线程?


答:主线程,分发器的入口方法 Activity#onBackPressed() 执行在主线程,因此回调方法也是执行在主线程。另外,添加回调的 addCallback() 方法也要求在主线程执行,分发器内部使用非并发安全容器 ArrayDeque 存储回调对象。


3.4 OnBackPressedCallback 可以同时添加到不同分发器吗?

答:可以。


3.5 加入返回栈的Fragment 事务,如何回退?


答:FragmentManager 也将事务回退交给 OnBackPressedDispatcher 处理。首先,在 Fragment attach 时,会创建一个回调对象加入分发器,回调处理时弹出返回栈栈顶事务。不过初始状态是未启用,只有当事务添加进返回栈后,才会修改回调对象为启用状态。源码体现如下:


FragmentManagerImpl.java


// 3.5.1 分发器与回调对象(初始状态是未启用)
private OnBackPressedDispatcher mOnBackPressedDispatcher;
private final OnBackPressedCallback mOnBackPressedCallback =
    new OnBackPressedCallback(false) {
        @Override
        public void handleOnBackPressed() {
            execPendingActions();
            if (mOnBackPressedCallback.isEnabled()) {
                popBackStackImmediate();
            } else {
                mOnBackPressedDispatcher.onBackPressed();
            }
        }
    };
// 3.5.2 添加回调对象 addCallback
public void attachController(@NonNull FragmentHostCallback host, @NonNull FragmentContainer container, @Nullable final Fragment parent) {
    if (mHost != null) throw new IllegalStateException("Already attached");
    ...
    // Set up the OnBackPressedCallback
    if (host instanceof OnBackPressedDispatcherOwner) {
        OnBackPressedDispatcherOwner dispatcherOwner = ((OnBackPressedDispatcherOwner) host);
        mOnBackPressedDispatcher = dispatcherOwner.getOnBackPressedDispatcher();
        LifecycleOwner owner = parent != null ? parent : dispatcherOwner;
        mOnBackPressedDispatcher.addCallback(owner, mOnBackPressedCallback);
    }
    ...
}
// 3.5.3 执行事务时,尝试修改回调对象状态
void scheduleCommit() {
     ...
    updateOnBackPressedCallbackEnabled();
}
private void updateOnBackPressedCallbackEnabled() {
    if (mPendingActions != null && !mPendingActions.isEmpty()) {
        mOnBackPressedCallback.setEnabled(true);
        return;
    }
    mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0 && isPrimaryNavigation(mParent));
}
// 3.5.4 回收
public void dispatchDestroy() {
    mDestroyed = true;
    ...
    if (mOnBackPressedDispatcher != null) {
        // mOnBackPressedDispatcher can hold a reference to the host
        // so we need to null it out to prevent memory leaks
        mOnBackPressedCallback.remove();
        mOnBackPressedDispatcher = null;
    }
}
复制代码


如果你对 Fragment 事务缺乏清晰的概念,务必看下我之前写的一篇文章:你真的懂 Fragment 吗?AndroidX Fragment 核心原理分析

讨论完 OnBackPressedDispatcher 的使用方法 & 实现原理,下面我们直接通过一些应用场景来实践:


4. 再按一次返回键退出


再按一次返回键退出是一个很常见的功能,本质上是一种退出挽回。网上也流传着很多不全面的实现方式。其实,这个功能看似简单,却隐藏着一些优化细节,一起来看看~


4.1 需求分析


首先,我分析了几十款知名的 App,梳理总结出 4 类返回键交互:


分类 描述 举例
1、系统默认行为 返回键事件交给系统处理,应用不做干预 微信、支付宝等
2、再按一次退出 是否两秒内再次点击返回键,是则退出 爱奇艺、高德等
3、返回首页 Tab 按一次先返回首页 Tab,再按一次退出 Facebook、Instagram等
4、刷新信息流 按一次先刷新信息流,再按一次退出 小红书、今日头条等

image.png


4.2 如何退出 App?


交互逻辑主要依赖于产品形态和具体应用场景,对于我们技术同学还需要考虑不同的退出 App 的方式的区别。通过观测以上 App 的实际效果,我梳理出以下 4 种退出 App 的实现方式:


  • 1、系统默认行为: 将回退事件交给系统处理,而系统的默认行为是 finish() 当前 Activity,如果当前 Activity 位于栈底,则将 Activity 任务栈转入后台;
  • 2、调用 moveTaskToBack(): 手动将当前 Activity 所在任务栈转入后台,效果与系统的默认行为类似(该方法接收一个 nonRoot 参数:true:要求只有当前 Activity 处于栈底有效、false:不要求当前 Activity 处于栈底)。因为 Activity 实际上并没有销毁,所以用户下次返回应用时是热启动;
  • 3、调用 finish(): 结束当前 Activity,如果当前 Activity 处于栈底,则销毁 Activity 任务栈,如果当前 Activity 是进程最后一个组件,则进程也会结束。需要注意的时,进程结束后内存不会立即被回收,将来(一段时间内)用户重新启动应用为温启动,启动速度比冷启动更快;
  • 4、调用 System.exit(0) 杀死应用 杀死进程 JVM,将来用户重新启动为冷启动,需要花费更多时间。


那么,我们应该如何选择呢?一般情况下,“调用 moveTaskToBack()” 表现最佳,两个论点:


  • 1、两次点击返回键的目的是挽回用户,确认用户真的需要退出。那么,退出后的行为与无拦截的默认行为相同,这点 moveTaskToBack() 可以满足,而 finish() 和 System.exit(0) 的行为比默认行为更严重;
  • 2、moveTaskToBack() 退出应用并没有真正销毁应用,用户重新返回应用是热启动,恢复速度最快。


需要注意,一般不推荐使用 System.exit(0) 和 Process.killProcess(Process.myPid) 来退出应用。因为这些 API 的表现并不理想:


  • 1、当调用的 Activity 不位于栈顶时,杀死进程系统会立即重新启动 App(可能是系统认为 前台 App 是意外终止的,会自动重启);
  • 2、当 App 退出后,粘性服务会自动重启(Service#onStartCommand() 返回 START_STICKY 的 Service),粘性服务会一致运行除非手动停止。


分类 应用返回效果 举例
1、系统默认行为 热启动 微信、支付宝等
2、调用 moveTaskToBack() 热启动 QQ 音乐、小红书等
3、调用 finish() 温启动 待确认(备选爱奇艺、高德等)
4、调用 System.exit(0) 杀死应用 冷启动 待确认(备选爱奇艺、高德等)

Process.killProcess(Process.myPid) 和 System.exit(0) 的区别? todo


4.3 具体代码实现


BackPressActivity.kt


fun Context.startBackPressActivity() {
    startActivity(Intent(this, BackPressActivity::class.java))
}
class BackPressActivity : AppCompatActivity(R.layout.activity_backpress) {
    // ViewBinding + Kotlin 委托
    private val binding by viewBinding(ActivityBackpressBinding::bind)
    /**
     * 上次点击返回键的时间
     */
    private var lastBackPressTime = -1L
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 添加回调对象
        onBackPressedDispatcher.addCallback(this, onBackPress)
        // 返回按钮
        binding.ivBack.setOnClickListener {
            onBackPressed()
        }
    }
    private val onBackPress = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (popBackStack()) {
                return
            }
            val currentTIme = System.currentTimeMillis()
            if (lastBackPressTime == -1L || currentTIme - lastBackPressTime >= 2000) {
                // 显示提示信息
                showBackPressTip()
                // 记录时间
                lastBackPressTime = currentTIme
            } else {
                //退出应用
                finish()
                // android.os.Process.killProcess(android.os.Process.myPid())
                // System.exit(0) // exitProcess(0)
                // moveTaskToBack(false)
            }
        }
    }
    private fun showBackPressTip() {
        Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show();
    }
}
复制代码


这段代码的逻辑并不复杂,我们主要通过 OnBackPressedDispatcher#addCallback() 添加了一个回调对象,从而干预了返回键事件的逻辑:“首次点击返回键弹出提示,两秒内再次点击返回键退出应用”。


另外,需要解释下这句代码: private val binding by viewBinding(ActivityBackpressBinding::bind)。这里其实是使用了 ViewBinding + Kotlin 委托属性的视图绑定方案,相对于传统的 findViewById、ButterKnife、Kotlin Synthetics 等方案,这个方案从多个角度上表现更好。具体分析你可以看我之前写过的一篇文章:Android | ViewBinding 与 Kotlin 委托双剑合璧


4.4 优化:兼容 Fragment 返回栈


上一节基本能满足需求,但考虑一种情况:页面内有多个 Fragment 事务加入了返回栈,点击返回键时需要先依次清空返回栈,最后再走 “再按一次返回键退出” 逻辑。

此时,你会发现上一节的方法不会等返回栈清空就直接走退出逻辑了。原因也很好理解,因为 Activity 的回退对象的加入时机比 FragmentManagerImpl 中的回退对象加入时机更早,所以 Activity 的回退逻辑优先处理。解决方法就是在 Activtiy 回退逻辑中手动弹出 Fragment 事务返回栈。完整演示代码如下:


BackPressActivity.kt


class BackPressActivity : AppCompatActivity(R.layout.activity_backpress) {
    private val binding by viewBinding(ActivityBackpressBinding::bind)
    /**
     * 上次点击返回键的时间
     */
    private var lastBackPressTime = -1L
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        addFragmentToStack()
        onBackPressedDispatcher.addCallback(this, onBackPress)
        binding.ivBack.setOnClickListener {
            onBackPressed()
        }
    }
    private fun addFragmentToStack() {
        // 提示:为了聚焦问题,这里不考虑 Activity 重建的场景
        for (index in 1..5) {
            supportFragmentManager.beginTransaction().let { it ->
                it.add(
                    R.id.container,
                    BackPressFragment().also { it.text = "fragment_$index" },
                    "fragment_$index"
                )
                it.addToBackStack(null)
                it.commit()
            }
        }
    }
    /**
     * @return true:没有Fragment弹出 false:有Fragment弹出
     */
    private fun popBackStack(): Boolean {
        // 当 Fragment 状态以保存,不弹出返回栈
        return supportFragmentManager.isStateSaved
                || supportFragmentManager.popBackStackImmediate()
    }
    private val onBackPress = object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (popBackStack()) {
                return
            }
            val currentTIme = System.currentTimeMillis()
            if (lastBackPressTime == -1L || currentTIme - lastBackPressTime >= 2000) {
                // 显示提示信息
                showBackPressTip()
                // 记录时间
                lastBackPressTime = currentTIme
            } else {
                //退出应用
                finish()
                // android.os.Process.killProcess(android.os.Process.myPid())
                // System.exit(0) // exitProcess(0)
                // moveTaskToBack(false)
            }
        }
    }
    private fun showBackPressTip() {
        Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show();
    }
}
复制代码


4.5 在 Fragment 中使用


TestFragment.kt


class TestFragment : Fragment() {
    private val dispatcher by lazy {requireActivity().onBackPressedDispatcher}
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        dispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                Toast.makeText(context, "TestFragment - handleOnBackPressed", Toast.LENGTH_SHORT).show()
            }
        })
    }
}
复制代码


4.6 其他 finish() 方法


另外,finish() 还有一些类似的 API,可以补充了解下:


  • finishAffinity():关闭当前 Activity 任务栈中,位于当前 Activity 底下的所有 Activity(例如 A 启动 B,B 启动 C,如果 B 调用 finishAffinity(),则会关闭 A 和 B,而 C 保留)。该 API 在 API 16 后引入,最好通过 ActivityCompat.finishAffinity() 调用。
  • finishAfterTransition():执行转场动画后 finish Activity,需要通过 ActivityOptions 定义转场动画。该 API 在 API 21 后引入,最好通过 ActivityCompat.finishAfterTransition() 调用。


5. 总结


关于 OnBackPressedDispatcher 的讨论就先到这里,给你留两个思考题:


  • 1、如果 Activity 上弹出一个 Dialog,此时点返回键是先关闭 Dialog,还是会分发给 OnBackPressedDispatcher?如果弹出的是 PopupWindow 呢?
  • 2、Activity 的 WebView 中弹出了一个浮层,怎么实现点击返回键先关闭浮层,再次点击才回退页面?
目录
相关文章
|
8月前
|
安全 API Android开发
Jetpack架构组件库-Jetpack入门介绍
Jetpack架构组件库-Jetpack入门介绍
101 0
|
存储 XML 安全
Jetpack DataStore 你总要了解一下吧?
一、DataStore 介绍 DataStore 是 Android Jetpack 中的一个组件,它是一个数据存储的解决方案,跟 SharedPreferences 一样,采用key-value形式存储。 DataStore 保证原子性,一致性,隔离性,持久性。尤其是,它解决了 SharedPreferences API 的设计缺陷。 Jetpack DataStore 是经过改进的新版数据存储解决方案,旨在取代 SharedPreferences,让应用能够以异步、事务方式存储数据。
890 0
Jetpack DataStore 你总要了解一下吧?
|
11月前
|
Java API 开发工具
Jetpack 之 LifeCycle 组件使用详解
LifeCycle 是一个可以感知宿主生命周期变化的组件。常见的宿主包括 Activity/Fragment、Service 和 Application。LifeCycle 会持有宿主的生命周期状态的信息,当宿主生命周期发生变化时,会通知监听宿主的观察者。
72 0
Jetpack 之 LifeCycle 组件使用详解
|
11月前
|
编解码
Jetpack 之 ViewModel 组件介绍
ViewModel 是介于 View(视图)和 Model(数据模型)之间的一个东西。它起到了桥梁的作用,使视图和数据既能够分离开,也能够保持通信。
58 0
Jetpack 之 ViewModel 组件介绍
|
11月前
|
Java API Android开发
Jetpack 之 LifeCycle 组件原理解析
1. LifeCycle 是如何监听到 Activity/Fragment 生命周期变化的? 2. LifeCycle 如何将生命周期变化的事件分发给观察者的?
87 0
Jetpack 之 LifeCycle 组件原理解析
|
XML JSON Java
Jetpack 系列之Paging3,看这一篇就够了~
Jetpack 系列之Paging3,看这一篇就够了~
3029 4
Jetpack 系列之Paging3,看这一篇就够了~
|
SQL 数据库 开发者
Jetpack初识
Google为了开发者更好的设计软件的代码架构以及写出高质量的代码,推出Jetpack组件,将许多好用的代码进行封装,总之使用Jetpack可以使得我们利用更少的时间开发出更高质量的软件
92 0
|
存储 安全
Jetpack之DataStore解析
前言在正式讲解DataStore之前,我们先回顾下大家熟知的SharedPreferences(以下简称SP),众所周知SP有一些缺点,如调用getXXX()获取数据,可能会阻塞主线程;无法保证类型安全;加载的数据会一直留在内存中,浪费内存;apply方法无法获取到操作成功或失败的结果。SharedPreferences回顾getXXX()可能会阻塞主线程请看以下SP获取value值的使用代码: 
266 0
Jetpack之DataStore解析
|
存储 安全 API
Jetpack之DataStore使用
前言DataStore提供了一种安全且持久的方式来存储少量数据。它不支持部分更新:如果任何字段被修改,整个对象将被序列化并持久到磁盘。它是线程安全的,非阻塞的。特别是,它解决了SharedPreferences这些设计缺陷:同步API鼓励违反StrictModeapply和commit没有发出错误信号的机制apply将阻塞fsync上的UI线程不持久-它可以返回尚未持久的状态没有一致性或事务语义在
220 0
Jetpack之DataStore使用
|
缓存 Java 编译器
【Jetpack】学穿:LiveData → ???(下)
在开始这篇文章前,我就遇到了第一个关于LiveData的问题:该怎么翻译这个词呢?
269 0