让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

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

保护原创,请勿转载!

相关推荐
烬奇小云3 小时前
认识一下Unicorn
android·python·安全·系统安全
顾北川_野15 小时前
Android 进入浏览器下载应用,下载的是bin文件无法安装,应为apk文件
android
CYRUS STUDIO15 小时前
Android 下内联汇编,Android Studio 汇编开发
android·汇编·arm开发·android studio·arm
右手吉他15 小时前
Android ANR分析总结
android
PenguinLetsGo17 小时前
关于 Android15 GKI2407R40 导致梆梆加固软件崩溃
android·linux
杨武博19 小时前
音频格式转换
android·音视频
音视频牛哥21 小时前
Android音视频直播低延迟探究之:WLAN低延迟模式
android·音视频·实时音视频·大牛直播sdk·rtsp播放器·rtmp播放器·android rtmp
ChangYan.1 天前
CondaError: Run ‘conda init‘ before ‘conda activate‘解决办法
android·conda
二流小码农1 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
夏非夏1 天前
Android 生成并加载PDF文件
android