在Kotlin中探索 Activity Results API 极简的解决方案
Activity Results API
Activity Result API提供了用于注册结果、启动结果以及在系统分派结果后对其进行处理的组件。—Google官方文档
https://developer.android.google.cn/training/basics/intents/result?hl=zh-cn
一句话解释:官方Jetpack组件用于代替startActivityForResult()/onActivityResult()。
看完文档会发现,能代替startActivityForResult(),但也并没有好用到哪去。
其实startActivityForResult()的调用并不麻烦,复杂页面的使用,做一下简单的封装即可。核心痛点在onActivityResult()的结果回调必须在Activity/Fragment中,导致我们在处理一些复杂的跳转逻辑时,总是要反复"横跳"。
Activity Result API的出现貌似可以解决这一痛点,页面返回结果直接通过回调就可以获得,还可以自定义跳转协议,进一步封装简化。
本来是那么的美好,然而在activity-ktx:1.2.0-beta02版本之后,变得让人望而却步。
https://developer.android.google.cn/jetpack/androidx/releases/activity?hl=zh-cn#1.2.0-beta02
行为变更
现在,尝试使用 Lifecycle 已达到 STARTED 的 LifecycleOwner 调用 register() 时,ActivityResultRegistry 会抛出 IllegalStateException。(b/165435866)
熟悉Activity Results API的都知道,页面返回结果的回调函数是在registerForActivityResult()方法里面的,这就导致了两个问题:
1. 跟startActivityForResult()/onActivityResult()一样的痛点:调launch跳转页面获取返回结果后还是要回到Activity/Fragment中处理。
2. 生命周期STARTED前注册,意味着我们必须提前注册而无法在点击使用时注册,只能在BaseActvity中封装。
但是遵循了高聚合低耦合的思想,封装在BaseActvity中的方案我们是万万拒绝的。
接下来我们就来探讨如何在不封装BaseActvity的情况下只调用一个带回调的函数实现startActivityForResult()/onActivityResult()。
解决思路
非Activity Results API方案
其实早在Activity Results API问世前,我们项目中就有使用一个空视图GhostFragment作为中转回调的方案来实现。
大概的思路如下:
Activty/Fragment——>add GhostFragment——>onAttach中startActivityForResult——>GhostFragment onActivityResult接收结果——>callback回调给Activty/Fragment
代码实现
GhostFragment.kt
https://github.com/iDeMonnnnnn/DeMon-ARA/blob/main/app/src/main/java/com/demon/ara/ghost/GhostFragment.kt
class GhostFragment : Fragment() {private var requestCode = -1private var intent: Intent? = nullprivate var callback: ((result: Intent?) -> Unit)? = nullfun init(requestCode: Int, intent: Intent, callback: ((result: Intent?) -> Unit)) {this.requestCode = requestCodethis.intent = intentthis.callback = callback}private var activityStarted = falseoverride fun onAttach(activity: Activity) {super.onAttach(activity)if (!activityStarted) {activityStarted = trueintent?.let { startActivityForResult(it, requestCode) }}}override fun onAttach(context: Context) {super.onAttach(context)if (!activityStarted) {activityStarted = trueintent?.let { startActivityForResult(it, requestCode) }}}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)if (resultCode == Activity.RESULT_OK && requestCode == this.requestCode) {callback?.let { it1 -> it1(data) }}}override fun onDetach() {super.onDetach()intent = nullcallback = null}
}Ghost.kt
https://github.com/iDeMonnnnnn/DeMon-ARA/blob/main/app/src/main/java/com/demon/ara/ghost/Ghost.kt
object Ghost {var requestCode = 0set(value) {field = if (value >= Integer.MAX_VALUE) 1 else value}inline fun launchActivityForResult(starter: FragmentActivity?,intent: Intent,crossinline callback: ((result: Intent?) -> Unit)) {starter ?: returnval fm = starter.supportFragmentManagerval fragment = GhostFragment()fragment.init(++requestCode, intent) { result ->callback(result)fm.beginTransaction().remove(fragment).commitAllowingStateLoss()}fm.beginTransaction().add(fragment, GhostFragment::class.java.simpleName).commitAllowingStateLoss()}}看到这里有同学就会质疑了,每次都添加一个Fragment就为了回调简化代码,这不浪费内存么?值得么?
第一次看到这个代码我也是迟疑的,直到我看了Glide的源码。
https://github.com/bumptech/glide
使用了Glide库的同学,开发中肯定有遇到如下报错:
You cannot start a load for a destroyed activity
放一个Glide源码的片段:
@NonNullpublic RequestManager get(@NonNull FragmentActivity activity) {if (Util.isOnBackgroundThread()) {return get(activity.getApplicationContext());} else {assertNotDestroyed(activity);frameWaiter.registerSelf(activity);FragmentManager fm = activity.getSupportFragmentManager();return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));}}@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)private static void assertNotDestroyed(@NonNull Activity activity) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) {throw new IllegalArgumentException("You cannot start a load for a destroyed activity");}} 看到这里大家应该是明白了,这个方案在Glide中被大家“发扬光大”了而已。
Activity Results API方案
再来思考一下如何使用Activity Results API实现。
根据前文提到的,Activity Results API我们想要去解决的两个问题:
• 回调最好能在launch中处理。
• 在Activity/Fragment中自动注册。
1. 回调改造在launch中处理
这里借鉴了优雅地封装 Activity Result API的思路,非常巧妙。
https://blog.csdn.net/c10WTiybQ1Ye3/article/details/119430078
DeMonActivityResult.kt
https://github.com/iDeMonnnnnn/DeMon-ARA/blob/main/core/src/main/java/com/demon/core/DeMonActivityResult.kt
class DeMonActivityResult<I, O>(caller: ActivityResultCaller, contract: ActivityResultContract<I, O>) {/*** 直接点击返回键或者直接finish是否会触发回调* 用于处理一些特殊情况:如只要返回就刷新等* 注意此时回调返回的值或者{ActivityResult#getData()}应该为空,需要做好判空处理*/private var isNeedBack = falseprivate var launcher: ActivityResultLauncher<I>? = caller.registerForActivityResult(contract) {if (isNeedBack) {callback?.onActivityResult(it)} else {if (it != null) {if (it is ActivityResult) {if (it.resultCode == Activity.RESULT_OK) callback?.onActivityResult(it)} else {callback?.onActivityResult(it)}}}callback = null}private var callback: ActivityResultCallback<O>? = null@JvmOverloadsfun launch(input: I, isNeedBack: Boolean = false, callback: ActivityResultCallback<O>?) {this.callback = callbackthis.isNeedBack = isNeedBacklauncher?.launch(input)}2. 在Activity/Fragment中自动注册
谈到Activity生命周期监听,有个始终绕不开的接口类Application.ActivityLifecycleCallbacks。
废话不多说,我要在onActivityCreatedregister,由于Activity Result API是自动反注册的,所以我们不用关心unRegister。
然后就是register后,怎么拿到ActivityResultLauncher呢?(经过上一步的改造后,我们需要拿到的是DeMonActivityResult)
恕在下才识浅薄,只能想到用HashMap。
//临时存储DeMonActivityResultval resultMap = mutableMapOf<String, DeMonActivityResult<Intent, ActivityResult>>()大概的思路如下:
onActivityCreated——>时间戳生成唯一key——>key putExtra存Activty——>register得到Result——>将Result与key存HashMap
Activty——>getStringExtra得key——>HashMap得Result——>Result.launch启动——>launch回调得返回结果
Fragment的生命周期监听与Activty类似,可以通过注册并实现抽象类FragmentLifecycleCallbacks:
//注册监听Fragment生命周期
activity.supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentCallbacks, false)
//反注册取消监听Fragment生命周期
activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(it)因此Fragment与Activity中的实现方法基本一致。
3. 实现代码
DeMonActivityCallbacks.kt
https://github.com/iDeMonnnnnn/DeMon-ARA/blob/main/core/src/main/java/com/demon/core/lifecycle/DeMonActivityCallbacks.kt
object DeMonActivityCallbacks : Application.ActivityLifecycleCallbacks {private val TAG = "DeMonActivityCallbacks"const val DEMON_ACTIVITY_KEY = "DeMon_Activity_Key"val DEMON_FRAGMENT_KEY = "DeMon_Fragment_Key"//临时存储FragmentCallbacksval callbackMap = mutableMapOf<String, DeMonFragmentCallbacks>()//临时存储DeMonActivityResultval resultMap = mutableMapOf<String, DeMonActivityResult<Intent, ActivityResult>>()override fun onActivityCreated(activity: Activity, p1: Bundle?) {if (activity is FragmentActivity) {val mapKey: String = activity.javaClass.simpleName + System.currentTimeMillis()Log.i(TAG, "onActivityCreated: mapKey=$mapKey")//注册val fragmentCallbacks = DeMonFragmentCallbacks()callbackMap[mapKey] = fragmentCallbacksactivity.supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentCallbacks, false)val result = DeMonActivityResult(activity, ActivityResultContracts.StartActivityForResult())activity.intent.putExtra(DEMON_ACTIVITY_KEY, mapKey)resultMap[mapKey] = result}}override fun onActivityDestroyed(activity: Activity) {if (activity is FragmentActivity) {val mapKey = activity.intent.getStringExtra(DEMON_ACTIVITY_KEY)Log.i(TAG, "onActivityDestroyed: mapKey=$mapKey")if (!mapKey.isNullOrEmpty()) {callbackMap[mapKey]?.let { activity.supportFragmentManager.unregisterFragmentLifecycleCallbacks(it) }//移除callbackMap.remove(mapKey)resultMap.remove(mapKey)}}}override fun onActivityStarted(p0: Activity) {}override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) {}override fun onActivityResumed(p0: Activity) {}override fun onActivityPaused(p0: Activity) {}override fun onActivityStopped(p0: Activity) {}
}篇幅原因Fragment生命周期监听和实现可见:DeMonFragmentCallbacks.kt。
https://github.com/iDeMonnnnnn/DeMon-ARA/blob/main/core/src/main/java/com/demon/core/lifecycle/DeMonFragmentCallbacks.kt
我们这里固定注册的是ActivityResultContracts.StartActivityForResult(),可能又会又同学觉得这样无法自定义跳转协定,太不灵活了。
其实不然,我们可以封装扩展Intent,比如大神陈小缘的ActivityMessenger。
https://github.com/Ifxcyr/ActivityMessenger
我们这个库也是按照这个思路对Intent进行了扩展,使用起来一样很方便,可以看本库的源码 。
https://github.com/iDeMonnnnnn/DeMon-ARA
接下来我们只需要在Application中:registerActivityLifecycleCallbacks(DeMonActivityCallbacks)即可。
值得注意的是registerActivityLifecycleCallbacks每次调用就是在回调集合中添加一个ActivityLifecycleCallbacks对象,集合中的每个ActivityLifecycleCallbacks都可以收到回调,因此可以注册多个。
简单处理一下获取DeMonActivityResult的逻辑:
@JvmStatic
fun getActivityResult(@NonNull activity: FragmentActivity): DeMonActivityResult<Intent, ActivityResult>? {activity.run {val mapKey = intent.getStringExtra(DeMonActivityCallbacks.DEMON_ACTIVITY_KEY)return if (!mapKey.isNullOrEmpty()) {DeMonActivityCallbacks.resultMap[mapKey]} else {null}}
}接下来我们可以在Activty/Fragment按照如下Java代码中使用即可:
DeMonActivityResult<Intent, ActivityResult> result = DeMonAraHelper.getActivityResult(JavaActivity.this);
if (result != null) {result.launch(new Intent(this, TestJumpActivity.class), true,data -> {if (data.getData() != null) {String str = data.getData().getStringExtra("tag");binding.text.setText("跳转页面返回值:" + str);} else {binding.text.setText("我是返回键返回的,没有返回值~");}});
} 走到这里我们就实现了我们最初的目标:调用一个带回调的函数实现
startActivityForResult()/onActivityResult()。
而且如果是Kotlin中进一步扩展调用只会更简单,如:
forActivityResult(pairIntent<ActResultActivity>()) {val str = it?.getStringExtra("tag") ?: ""text.text = "跳转页面返回值:$str"
}Benchmark
我们简单测试一下以下四种方式直接执行100次时的性能。
测试代码可见:BenchmarkActivity.kt
https://github.com/iDeMonnnnnn/DeMon-ARA/blob/main/app/src/main/java/com/demon/ara/BenchmarkActivity.kt
测试机型:小米5

内存方面:测试过程中使用AndroidStudio Profiler监测的内存波动基本一致。
源码
文章中的所以代码都可见:
DeMon-ARA
https://github.com/iDeMonnnnnn/DeMon-ARA
参考&致谢
优雅地封装 Activity Result API
https://blog.csdn.net/c10WTiybQ1Ye3/article/details/119430078
ActivityMessenger
https://github.com/Ifxcyr/ActivityMessenger
Glide
https://github.com/bumptech/glide
相关文章:
在Kotlin中探索 Activity Results API 极简的解决方案
Activity Results APIActivity Result API提供了用于注册结果、启动结果以及在系统分派结果后对其进行处理的组件。—Google官方文档https://developer.android.google.cn/training/basics/intents/result?hlzh-cn一句话解释:官方Jetpack组件用于代替startActivity…...
样式冲突太多,记一次前端CSS升级
目前平台前端使用的是原生CSSBEM命名,在多人协作的模式下,容易出现样式冲突。为了减少这一类的问题,提升研效,我调研了业界上主流的7种CSS解决方案,并将最终升级方案落地到了工程中。 样式冲突的原因 目前遇到的样式…...
如何解决报考PMP的那些问题?
关于PMP的报考条件,报考PMP都需要什么条件呢?【学历条件】:需要满足23周岁/高中毕业5年以上/大专以上学历,三个满足一个即可;【PDU条件】:报考PMP需要PDU证明(学习项目管理课程的学时证明&#…...
数据结构栈的经典OJ题【leetcode最小栈问题大剖析】【leetcode有效的括号问题大剖析】
目录 0.前言 1.最小栈 1.1 原题展示 1.2 思路分析 1.2.1 场景引入 1.2.2 思路 1.3 代码实现 1.3.1 最小栈的删除 1.3.2 最小栈的插入 1.3.3 获取栈顶元素 1.3.4 获取当前栈的最小值 2. 有效的括号 0.前言 本篇博客已经把两个关于栈的OJ题分块,可以根据目…...
数据结构与算法之打家劫舍(一)动态规划思想
动态规划里面一部题目打家劫舍是一类经典的算法题目之一,他有各种各样的变式,这一篇文章和大家分享一下打家劫舍最基础的一道题目,掌握这一道题目,为下一道题目打下基础。我们直接进入正题。一.题目大家如果刚接触这样的题目&…...
无人驾驶路径规划论文简要
A Review of Motion Planning Techniques for Automated Vehicles综述和分类0Motion Planning for Autonomous Driving with a Conformal Spatiotemporal Lattice从unstructured环境向structured环境的拓展,同时还从state lattice拓展到了spatiotemporal lattice从而…...
C++ sort()函数和priority_queue容器中比较函数的区别
普通的queue是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。priority_queue中元素被赋予优先级。在创建的时候根据优先级进行了按照从大到小或者从小到大进行了自动排列(大顶堆or小顶堆)。可以以O(log n) 的效率查找…...
STM32开发(14)----CubeMX配置ADC
CubeMX配置ADC前言一、什么是ADC?二、实验过程1.单通道ADC采集STM32CubeMX配置代码实现2.多通道ADC采样(非DMA)STM32CubeMX配置代码实现3.多通道ADC采样(DMA)STM32CubeMX配置代码实现总结前言 本章介绍使用STM32CubeMX对ADC进行配置的方法&a…...
Simple RNN、LSTM、GRU序列模型原理
一。循环神经网络RNN 用于处理序列数据的神经网络就叫循环神经网络。序列数据说直白点就是随时间变化的数据,循环神经网络它能够根据这种数据推出下文结果。RNN是通过嵌含前一时刻的状态信息实行训练的。 RNN神经网络有3个变种,分别为Simple RNN、LSTM、…...
【原创】java+swing+mysql生肖星座查询系统设计与实现
今天我们来开发一个比较有趣的系统,根据生日查询生肖星座,输入生日,系统根据这个日期自动计算出生肖和星座信息反馈到界面。我们还是使用javaswingmysql去实现这样的一个系统。 功能分析: 生肖星座查询系统,顾名思义…...
CentOS 环境 OpneSIPS 3.1 版本安装及使用
文章目录1. OpenSIPS 源码下载2. 工具准备3. 编译安装4. opensips-cli 工具安装5. 启动 OpenSIPS 实例1. OpenSIPS 源码下载 使用以下命令即可下载 OpenSIPS 的源码,笔者下载的是比较稳定的 3.1 版本,读者有兴趣也可前往 官方传送门 sudo git clone htt…...
SQL95 从 Products 表中检索所有的产品名称以及对应的销售总数
描述 Products 表中检索所有的产品名称:prod_name、产品id:prod_idprod_idprod_namea0001egga0002socketsa0013coffeea0003colaOrderItems代表订单商品表,订单产品:prod_id、售出数量:quantityprod_idquantitya0001105…...
平时技术积累很少,面试时又会问很多这个难题怎么破?别慌,没事看看这份Java面试指南,解决你的小烦恼!
前言技术面试是每个程序员都需要去经历的事情,随着行业的发展,新技术的不断迭代,技术面试的难度也越来越高,但是对于大多数程序员来说,工作的主要内容只是去实现各种业务逻辑,涉及的技术难度并不高…...
SQL Server 数据库的备份
为何要备份数据库? 备份 SQL Server 数据库、在备份上运行测试还原过程以及在另一个安全位置存储备份副本可防止可能的灾难性数据丢失。 备份是保护数据的唯一方法 。 使用有效的数据库备份,可从多种故障中恢复数据,例如: 介质…...
NCNN Conv量化详解1
1. NCNN的Conv量化计算流程 正常的fp32计算中,一个Conv的计算流程如下: 在NCNN Conv进行Int8计算时,计算流程如下: NCNN首先将输入(bottom_blob)和权重(weight_blob)量化成INT8,在INT8下计算卷积,然后反量化到fp32,再和未量化的bias相加,得到输出(top_blob) 输入和…...
Redis大key多key拆分方案
业务场景中经常会有各种大key多key的情况, 比如:1:单个简单的key存储的value很大2:hash, set,zset,list 中存储过多的元素(以万为单位)3:一个集群存储了上亿的…...
python的类如何使用?兔c同学一篇关于python类的博文概述
本章内容如目录 所示: 文章目录1. 创建和使用类1.1 创建第一个python 类1.2 版本差异1.3 根据类创建实例1. 访问属性2. 调用方法3. 创建多个实例2. 使用类和实例2.1 给属性指定默认值2.2 修改属性的值3. 继承3.1 子类的 __init __()3.2 给子类定义属性和方法3.3 重写…...
Day60 动态规划总结
647. 回文子串 回文的做法注定我们得从里面入手,逐渐扩散到边界 初始化:准备一个ans,找到一个回文子串加一个 dp [[0] * n for _ in range(n)]ans 0 遍历公式: 当s[i]s[j]的时候,只要里面还是回文串,就能…...
UVM仿真环境搭建
环境 本实验使用环境为: Win10平台下的Modelsim SE-64 2019.2 代码 dut代码: module dut(clk,rst_n, rxd,rx_dv,txd,tx_en); input clk; input rst_n; input[7:0] rxd; input rx_dv; output [7:0] txd; output tx_en;reg[7:0] txd; reg tx_en;always…...
Azure AI基础到实战(C#2022)-认知服务(1)
目录 Azure 认知服务概述计算机视觉概述数据隐私和安全性计算机视觉快速入门光学字符识别 (OCR)OCR APIOCR 常用功能Azure 门户准备两种部署方式OCR项目实战之车牌识别Azure 认知服务概述 Azure 认知服务是基于云的人工智能 (AI) 服务,可帮助开发人员在不具备直接的 AI 或数据…...
【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型
摘要 拍照搜题系统采用“三层管道(多模态 OCR → 语义检索 → 答案渲染)、两级检索(倒排 BM25 向量 HNSW)并以大语言模型兜底”的整体框架: 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后,分别用…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
智能在线客服平台:数字化时代企业连接用户的 AI 中枢
随着互联网技术的飞速发展,消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁,不仅优化了客户体验,还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用,并…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...
VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
