在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 或数据…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
mongodb源码分析session执行handleRequest命令find过程
mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程,并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令,把数据流转换成Message,状态转变流程是:State::Created 》 St…...
el-switch文字内置
el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
成都鼎讯硬核科技!雷达目标与干扰模拟器,以卓越性能制胜电磁频谱战
在现代战争中,电磁频谱已成为继陆、海、空、天之后的 “第五维战场”,雷达作为电磁频谱领域的关键装备,其干扰与抗干扰能力的较量,直接影响着战争的胜负走向。由成都鼎讯科技匠心打造的雷达目标与干扰模拟器,凭借数字射…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...
10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
