让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中的ActivityFragment中,可以像常规的回调那样向系统注册一个处理result的回调,一旦系统派发了activity result就能被系统回调到。

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

注册一个activity result回调

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

Kotlin 复制代码
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就会被执行:

Kotlin 复制代码
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了。

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

Kotlin 复制代码
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的输入参数)。

Kotlin 复制代码
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对象:

Kotlin 复制代码
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即可:

Kotlin 复制代码
@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

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

保护原创,请勿转载!

相关推荐
TroubleMaker1 小时前
OkHttp源码学习之retryOnConnectionFailure属性
android·java·okhttp
叶羽西3 小时前
Android Studio IDE环境配置
android·ide·android studio
发飙的蜗牛'3 小时前
23种设计模式
android·java·设计模式
花追雨12 小时前
Android -- 双屏异显之方法一
android·双屏异显
小趴菜822712 小时前
安卓 自定义矢量图片控件 - 支持属性修改矢量图路径颜色
android
氤氲息12 小时前
Android v4和v7冲突
android
KdanMin12 小时前
高通Android 12 Launcher应用名称太长显示完整
android
chenjk412 小时前
Android不可擦除分区写文件恢复出厂设置,无法读写问题
android
袁震12 小时前
Android-Glide缓存机制
android·缓存·移动开发·glide
工程师老罗13 小时前
Android笔试面试题AI答之SQLite(2)
android·jvm·sqlite