有过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:
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
- The Usage of Activity Result Launcher
- Get a result from an activity
- Jetpack Compose: Launch ActivityResultContract request from Composable function
- Compose and other libraries
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!