让Activity更加优雅地跳转

news2025/1/12 18:17:00

公众号「稀有猿诉」        原文链接 让Activity更加优雅地跳转

有过Android开发经验的小伙伴对startActivityForResult以及onActivityResult一定不陌生,正是这一对API让组件 的复用变成可能。今天就来学习一下如何在函数式的范式中驾驭Activity的跳转。
在这里插入图片描述

缘起

系统组件复用,特别是Activity的复用,是Android系统中非常重要的一个设计理念。组件复用打破了应用程序之间的壁垒,在整个系统范围内可以共享和复用一些公共的组件,比如像打开网页,拍照片,查看图片等等,开发者不必再用原始API去实现一套,直接使用startActivityForResult和onActivityResult就可以取到需要的资源。

这套API最大的问题在于它并不是常规的异步式的回调,调用了startActivityForResult后,结果的处理,必须要在Activity的继承体系内覆写onActivityResult,并且因为Activity实例只能由系统创建,这就导致了组件复用的逻辑必须都在Activity内部。这就导致了Activity的体积通常会相当的臃肿,上千行,甚至大几千行的Activity随处可见。理想的情况下Activity,作为一个系统的容器和接口,应该越薄越好,但要能把逻辑移出Activity才行。

另一方面,onActivityResult无法在函数式的情境中使用,因为它会跑到函数外面去,比如在Jetpack Compose中就无法直接使用startActivityForResult和onActivityResult。

为了解决这两个问题,就需要使用到Jetpack中的Activity Result API了。

Activity Result API的使用方法

在Jetpack的AndroidX中的Activity和Fragment中,可以像常规的回调那样向系统注册一个处理result的回调,一旦系统派发了activity result就能被系统回调到。

注意: 这里提到的方法都在AndroidX中的ComponentActivity和Fragment里面,也就是说要继承AndroidX中的组件才可以。

注册一个activity result回调

这套API的方式是在ComponentActivity和Fragment中,提供了一个registerForActivityResult方法用于注册activity result的回调。参数是一个ActivityResultContract实例和一个ActivityResultCallback实例。返回的是一个ActivityResultLauncher,这个launcher可以用来启动目标Activity,也即触发获取资源的流程,相当于原来的startActivityForResult:

val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
    // 处理结果
}

一个ActivityResultContract,如它的名字所示,定义着组件复用的的接口,即输入类型和输出类型。API中定义了大量的现成可用的,也是常见的接口,比如拍照,权限请求等等。当然也可以创建自定义接口。

回调ActivityResultCallback是只有一个方法onActivityResult()的接口,此方法的参数由ActivityResultContract来定义。

启动目标Activity

当调用registerForActivityResult时,能拿到一个launcher,但此API仅是向系统注册一个回调,这时还没有启动目标(即还没有发起请求)。发起请求需要使用ActivityResultLauncher来完成。

调用其方法launch就会发起请求,启动目标Activity,开启获取结果的流程。如果给launch传递了参数,会依据ActivityResultContract做进一步的匹配(其实这些输入最终会转化为Intent对象提供给startActivityForResult)。用户在目标Activity页面完成了操作后,就会返回到当前页面,回调ActivityResultCallback的方法onActivityResult就会被执行:

val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
    // 处理结果,即返回的Uri
}

override fun onCreate(savedInstanceState: Bundle?) {
    // ...

    val selectButton = findViewById<Button>(R.id.select_button)

    selectButton.setOnClickListener {
        // 接口是获取内空,这里传mime type作为参数,那么就是要获取一个图片内容
        getContent.launch("image/*")
    }
}

如果需要多个组件复用,那就传递不同的参数多次调用registerForActivityResult。并且registerForActivityResult可以在任何时候调用,在onCreate之前调用也是安全的,所以可以在声明ActivityResultLauncher的时候就直接调用,这样可以直接初始化。

但是要特别注意,使用launcher来启动Activity则必须在onCreate之后。

还有一点需要特别注意,因为launch之后,onActivityResult之前这段时间会离开当前的Activity,这个时间内Activity可能会被系统回收,也即触发了状态恢复。所以处理结果时,也即onActivityResult中的逻辑,如果有依赖其他状态,这些状态需要在onSaveInstanceState中进行保存。

处理结果

结果的处理就在ActivityResultCallback中的方法onActivityResult,这里使用返回的参数就可以了。

在Activity之外使用

如前面所述,使用这套Result API的最大的好处在于把结果的处理从Activity中解耦出来,因此,最为理想的方式是能在独立的class中做这些事情。

这就需要使用ActivityResultRegistry,它才是核心,另外三个类(launcher,contract和callback)都是一些封装,事实上Activity和Fragment里面的方法registerForActivityResult其实也是使用这个registry来实现的。从Activity中可以拿到registry的实例,以此作为参数,就可以在自定义的class中使用Result APIs了。

比如单独封装获取图片的流程可以这样写:

class MyLifecycleObserver(private val registry : ActivityResultRegistry)
        : DefaultLifecycleObserver {
    lateinit var getContent : ActivityResultLauncher<String>

    override fun onCreate(owner: LifecycleOwner) {
        getContent = registry.register("key", owner, GetContent()) { uri ->
            // Handle the returned Uri
        }
    }

    fun selectImage() {
        getContent.launch("image/*")
    }
}

class MyFragment : Fragment() {
    lateinit var observer : MyLifecycleObserver

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        observer = MyLifecycleObserver(requireActivity().activityResultRegistry)
        lifecycle.addObserver(observer)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val selectButton = view.findViewById<Button>(R.id.select_button)

        selectButton.setOnClickListener {
            // 触发获取图片的流程
            observer.selectImage()
        }
    }
}

这个示例把获取图片的流程(发起和结果处理)都封装在了一个单独的类中,同时又是明是监听了Activity组件的生命周期。谷歌是强烈建议同时要监听生命周期(通过扩展LifecycleObserver),这是因为LifecycleOwner会在destroy时自动帮你反注册ActivityResultLauncher,不然的话就要手动的反注册。

自定义Contract

尽管谷歌已经在ActivityResultContracts中已经预定义了大量的contracts可以使用,但仍然会有一些特殊的场景因预定义的contract无法满足需求而需要自定义一个contract。这个contract实际上就是约定了组件复用的接口,就像普通的interface一样,定义好输入与输出的类型就可以了,所以需要给contract提供输入输出的类型,如果不需要输入或者输出就使用Void?或者Unit。

此外还需要实现一个createIntent方法,这个方法接收一个Context和其他输入(即contract约定的输入,最终是由ActivityResultLauncher中方法launch时提供)作为参数并返回一个Intent对象,此Intent会是startActivityForResult的输入参数。同时还需要实现另外一个方法parseIntent,此方法将Activity的标准钩子onActivityResult中的参数resultCode和Intent转化为contract中约定的输出(此输出会作为回调ActivityResultCallback函数方法onActivityResult的输入参数)。

class PickRingtone : ActivityResultContract<Int, Uri?>() {
    override fun createIntent(context: Context, ringtoneType: Int) =
        Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
            putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
        }

    override fun parseResult(resultCode: Int, result: Intent?) : Uri? {
        if (resultCode != Activity.RESULT_OK) {
            return null
        }
        return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
    }
}

如果现有的contracts不满足需求,且也无具体的输入输出要求,那么可以用一个万用contract,即StartActivityForResult。这个万用contract的输入是一个Intent,输出是一个ActivityResult,在回调方法onActivityResult中可以直接从ActivityResult实例中取出resultCode和目标返回的Intent对象:

val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
    if (result.resultCode == Activity.RESULT_OK) {
        val intent = result.data
        // 处理目标返回的Intent
    }
}

override fun onCreate(savedInstanceState: Bundle) {
    // ...

    val startButton = findViewById(R.id.start_button)

    startButton.setOnClickListener {
        // 传入想要启动的Intent对象
        startForResult.launch(Intent(this, ResultProducingActivity::class.java))
    }
}

从这里我们可以看出,这套Result API本质上仍是依赖于原始的startActivityForResult和onActivityResult。

在Compose中使用Result API

接下来我们看看如何在Jetpack Compose使用这套API,这套API与Activity彻底解耦且支持函数式写法,所以可以在Compose中使用。这套API的核心是ActivityResultRegistry,有了它其他几个就可以使用起来了,而它的实例可以直接从Activity中取出来,所以这套API在Compose中完全可以用起来,与前面讲到的在Activity之外的逻辑完全一样:获取此对象用于register一个contract,同时得到一个launcher对象,在回调中处理结果,在合适的时机触发launch。

幸运的是完全用不着自己折腾,Compose中已经做好了封装,直接使用rememberLauncherForActivityResult即可:

@Composable
fun GetContentExample() {
    var imageUri by remember { mutableStateOf<Uri?>(null) }
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
        imageUri = uri
    }
    Column {
        Button(onClick = { launcher.launch("image/*") }) {
            Text(text = "Load Image")
        }
        Image(
            painter = rememberAsyncImagePainter(imageUri),
            contentDescription = "My Image"
        )
    }
}

今天我们学习了Jetpack中提供的新式处理activity result的方法,这不仅能让在函数式编程范式中复用组件变成可能,也可以把很多逻辑从Activity中抽离出来,能给Activity瘦身,让组件跳转变得更为优雅。

References

  • The Usage of Activity Result Launcher
  • Get a result from an activity
  • Jetpack Compose: Launch ActivityResultContract request from Composable function
  • Compose and other libraries

subscription

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2215344.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

git的基本操作 + 分支管理

一、基本操作 1. 修改文件 Git比其他的版本管理器设计得更加优秀&#xff0c;因为Git追踪并管理的是修改&#xff0c;而非文件。 修改一个文件&#xff0c;不管你是添加一行&#xff0c;或者删除一行&#xff0c;还是添加了又删除了&#xff0c;甚至你创建了一个新文件&…

《CS:GO》的标志性实验地图在 RTX GPU 神经网络中运行

研究人员让标志性的《CS:GO》地图《Dust 2》完全通过神经网络在单个 RTX 3090 GPU 上模拟运行。 虽然这些片段既令人印象深刻&#xff0c;又问题百出&#xff0c;但它们展示了生成式人工智能在模仿全 3D 游戏环境方面取得的令人敬佩的进展。 该项目的负责人之一埃洛伊-阿隆索&a…

了解CRM外贸管理软件及其应用指南

在全球化的商业环境中&#xff0c;外贸活动显得尤为重要&#xff0c;而有效地管理这些活动则成为企业取得成功的关键之一。CRM外贸管理软件作为一种专门针对外贸企业设计的客户关系管理工具&#xff0c;近年来受到越来越多国际贸易公司的青睐。其中&#xff0c;Zoho CRM作为市场…

Java项目:157 基于springboot技术的美食烹饪互动平台的设计与实现(含论文+说明文档)

作者主页&#xff1a;源码空间codegym 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 本知识管理系统有管理员和用户两个角色 管理员权限操作的功能包括管理美食&#xff0c;对美食留言进行回复&#xff0c;管理美食知识信息&…

【QAMISRA】解决导入commands.json时报错问题

1、 文档目标 解决导入commands.json时报错“Could not obtain system-wide includes and defines”的问题。 2、 问题场景 客户导入commands.json时报错“Could not obtain system-wide includes and defines”。 3、软硬件环境 1、软件版本&#xff1a; QA-MISRA23.04 2、…

9.7 栅格图层符号化轮廓渲染

9.7 栅格图层符号化轮廓渲染_栅格构建轮廓-CSDN博客 目录 前言 轮廓 QGis设置为轮廓 二次开发代码实现轮廓 总结 前言 介绍栅格图层数据渲染之轮廓渲染说明&#xff1a;文章中的示例代码均来自开源项目qgis_cpp_api_apps 轮廓 以“3420C_2010_327_RGB_LATLNG.tif”数据…

UE小:UE5的Pixelstreaming在捕获画面的时候没办法显示非Viewport的Slate区域按钮的ToolTip

原始代码 首先&#xff0c;让我们看看原始代码片段&#xff1a; // Some widgets might want to provide an alternative Tooltip Handler. if (bCanSpawnNewTooltip || !NewTooltip) {TSharedPtr<SWidget> NewTooltipWidget NewTooltip ? NewTooltip->AsWidget()…

【MM2024】阿里云 PAI 团队图像编辑算法论文入选 MM2024

近期&#xff0c;阿里云人工智能平台 PAI 团队发表的图像编辑算法论文在 MM2024 上正式亮相发表。ACM MM&#xff08;ACM国际多媒体会议&#xff09;是国际多媒体领域的顶级会议&#xff0c;旨在为研究人员、工程师和行业专家提供一个交流平台&#xff0c;以展示在多媒体领域的…

lstm基础知识

lstm前言 LSTM(Long short-term memory)通过刻意的设计来避免长期依赖问题&#xff0c;是一种特殊的RNN。长时间记住信息实际上是 LSTM 的默认行为&#xff0c;而不是需要努力学习的东西&#xff01; 在标准的RNN中&#xff0c;这个重复模块具有非常简单的结构&#xff0c;例…

TikTok Shop菲律宾卖家突破200万,TikTok自动批量关注、点赞、留言和私信

TikTok Shop自进军菲律宾市场以来&#xff0c;积极推动“购物娱乐”的概念&#xff0c;迅速成为当地电商领域的一股重要力量。其GMV&#xff08;商品交易总额&#xff09;和销售额在菲律宾市场占据显著份额&#xff0c;显示出强大的市场影响力和增长潜力。 相关数据显示&#…

TS中如何正确处理window类型

在Typescript项目中&#xff0c;你可能都遇到过这个错误&#xff1a; Window & typeof globalThis 类型上不存在属性 X。 快速修复方案 我们将介绍几种不同的解决方案来解决这个问题。 Window 接口是在名为 lib.dom.d.ts 的文件中全局定义的。你可以使用各种技术来更改它&a…

木材密度计的用途与试样硬度测量细节须知

木材密度计的重要性 在木材行业中&#xff0c;木材密度计是一种不可或缺的工具。它能够准确测量木材的密度&#xff0c;为木材的质量评估、分类以及加工提供关键的数据支持。 木材密度计的用途 首先&#xff0c;在木材贸易中&#xff0c;密度是决定木材价格和品质的重要因素…

排序算法详解~(更新中)

稳定性 在排序算法中&#xff0c;稳定性是一个重要的概念&#xff0c;指的是在排序过程中&#xff0c;如果两个元素的值相等&#xff0c;它们在排序后的相对位置与排序前的相对位置保持不变的特性。 稳定排序与不稳定排序 稳定排序&#xff1a;在排序时&#xff0c;相等的元素…

springboot 修复 Spring Framework 特定条件下目录遍历漏洞(CVE-2024-38816)

一、漏洞描述 Spring框架是 Java 平台的一个开源的全栈应用程序框架和控制反转容器实现。2024年9月&#xff0c;Spring官方发布公告披露 CVE-2024-38816 Spring Framework 特定条件下目录遍历漏洞。当同时满足使用 RouterFunctions 和 FileSystemResource 来处理和提供静态文件…

海外问卷口子查是什么?好做吗?

先给出结论&#xff0c;口子查不好做。 大家好&#xff0c;我是橙河老师&#xff0c;今天讲一讲海外问卷口子查是什么&#xff0c;好做吗&#xff1f;我自己做海外问卷项目已经4年时间了&#xff0c;我做过口子查、站点查&#xff0c;现在一直做的是渠道查。 橙河老师在一个行…

小红书新ID保持项目StoryMaker,面部特征、服装、发型和身体特征都能保持一致!(已开源)

继之前和大家介绍的小红书在ID保持以及风格转换方面相关的优秀工作&#xff0c;感兴趣的小伙伴可以点击以下链接阅读~ 近期&#xff0c;小红书又新开源了一款文生图身份保持项目&#xff1a;StoryMaker&#xff0c;是一种个性化解决方案&#xff0c;它不仅保留了面部的一致性&…

一文搞懂H100/H200,B100/B200,B200/GB200,HGX/DGX的区别和参数

前言&#xff0c;最近英伟达GPU热点一直很高&#xff0c;尤其是对H200&#xff0c;B200的讨论&#xff0c;当然也包括GB200&#xff0c;DGX及HGX等&#xff0c;我简单汇总了以下几个问题&#xff0c;我们今天展开聊聊&#xff01; 1、你清楚H200比H100升级了什么吗&#xff1f;…

Google DeepMind提出RAG推理 scaling laws下的思考

推理计算的扩展释放了长文本大语言模型&#xff08;LLM&#xff09;在各种环境中的潜力。对于知识密集型任务&#xff0c;增加的计算量通常被分配用于纳入更多外部知识。然而&#xff0c;如果不能有效利用这些知识&#xff0c;仅仅扩展上下文并不总能提高性能。 Google DeepMi…

【SRE系列--DNS跨域转发】

1.DNS原理 1.1 简介 DNS(Domain Name Service的缩写)的作用就是根据域名查出IP地址。IP地址是由32位二进制数字组成&#xff0c;人们很难记住这些IP&#xff0c;相反&#xff0c;大家愿意使用比较容易记忆的主机名字。而电脑在处理IP数据报文时&#xff0c;是使用IP地址的&am…