让Activity更加优雅地跳转

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

header

缘起

系统组件复用,特别是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:

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? ->
    
}

override fun onCreate(savedInstanceState: Bundle?) {
    

    val selectButton = findViewById

如果需要多个组件复用,那就传递不同的参数多次调用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

    override fun onCreate(owner: LifecycleOwner) {
        getContent = registry.register("key", owner, GetContent()) { 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

这个示例把获取图片的流程(发起和结果处理)都封装在了一个单独的类中,同时又是明是监听了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
        
    }
}

override fun onCreate(savedInstanceState: Bundle) {
    

    val startButton = findViewById(R.id.start_button)

    startButton.setOnClickListener {
        
        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(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

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

保护原创,请勿转载!

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.shuli.cc/?p=21407,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?