是时候丢掉 onActivityResult 了 !
为什么要丢掉 onActivityResult ?
如何启动一个新的 Activity,并获取返回值?
你的答案肯定是startActivityForResult和onActivityResult。没错,一直以来,在某些场景下,例如启动系统相机拍照,返回当前页面后获取照片数据,我们并没有其他选择,只能在 onActivityResult 中进行处理。
在最新的Activity 1.2.0-alpha02和Fragment 1.3.0-alpha02中,Google 提供了新的Activity Result API, 让我们可以更加优雅的处理 onActivityResult 。在介绍新 API 之前,我们不妨思考一下,为什么 Google 要丢掉 onActivityResult?
减少样板代码,解耦,更易测试。
举个最简单的场景,MainActivity跳转到SecondActivity,SecondActivity中按钮触发返回并传值回来。SecondActivity中的代码很简单:
class SecondActivity : AppCompatActivity(R.layout.activity_second){ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) back.setOnClickListener { setResult(Activity.RESULT_OK, Intent().putExtra("value","I am back !")) finish() } } } 复制代码
现在支持直接在AppCompatActivity()构造函数中传入 layoutId 了,无需另外setContentView()。
回到MainActivity中,按照传统的写法,是这样的:
class MainActivity : AppCompatActivity(R.layout.activity_main) { private val REQUEST_CODE = 1 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) jump.setOnClickListener { jump() } } private fun jump() { startActivityForResult(Intent(this, SecondActivity::class.java), REQUEST_CODE) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE) { toast(data?.getStringExtra("value") ?: "") } }API } 复制代码
定义一个 REQUEST_CODE ,同一页面有多个时,保证不重复调用 startActivityForResult在 onActivityResult 中接收回调,并判断 requestCode,resultCode
上面的逻辑中不乏重复的样板代码,且大多都耦合在视图控制器(Activity/Fragment)中,也就造成了不易测试。细品一下,的确不是那么的合理。
可能一直以来我们也只有这一个选择,所以也很少看到有人抱怨 onActivityResult。精益求精的 Google 工程师为我们改进了这一问题。
下面来看看如何使用最新的 Activity Result API 。
Activity Result API
private val startActivity = prepareCall(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult? -> toast(result?.data?.getStringExtra("value") ?: "") } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) jump.setOnClickListener { jump() } } private fun jump() { startActivity.launch(Intent(this,SecondActivity::class.java)) } 复制代码
恩,就是这么简单。主要就两个方法,prepareCall()和launch()。拆解开来逐一分析。
public <I, O> ActivityResultLauncher<I> prepareCall( @NonNull ActivityResultContract<I, O> contract, @NonNull ActivityResultCallback<O> callback) { return prepareCall(contraguanxict, mActivityResultRegistry, callback); } 复制代码
prepare()方法接收两个参数,ActivityResultContract和ActivityResultCallback,返回值是ActivityResultLauncher。这几个名字取得都很好,见名知意。
ActivityResultContract
ActivityResultContract可以理解为一种协议,它是一个抽象类,提供了两个能力,createIntent和parseResult。这两个能力放到启动 Activity 中就很好理解了,createIntent 负责为 startActivityForResult 提供 Intent ,parseResult 负责处理 onActivityResult 中获取的结果。
上面的例子中,prepare() 方法传入的协议实现类是StartActivityForResult。它是ActivityResultContracts类中的静态内部类。除了StartActivityForResult之外,官方还默认提供了RequestPermissions,Dial,RequestPermission,TakePicture,它们都是ActivityResultContract的实现类。
所以,除了可以简化startActivityForResult,权限请求,拨打电话,拍照,都可以通过 Activity Result API 得到了简化。除了使用官方默认提供的这些之外,我们还可以自己实现 ActivityResultContract,在后面的代码中会进行演示。
ActivityResultCallback
public interface ActivityResultCallback<O> { /** * Called when result is available */ void onActivityResult(@SuppressLint("UnknownNullness") O result); } 复制代码
这个就比较简单了。当回调结果可用时,通过该接口通知。需要注意的一点是,由于 prepare() 方法的泛型限制,这里的返回值 result 一定是类型安全的。下表是系统内置协议和其返回值类型的对应关系。
Github
协议类型
返回值类型
StartActivityForResult ActivityResult
TakePicture Bitmap
Dial Boolean
RequestPermission Boolean
RequestPermissions Map<String,Boolean>
ActivityResultLauncher
prepare()方法的返回值。
prepare()方法其实会调用ActivityResultRegistry.registerActivityResultCallback()方法,具体的源码这里就不分析了,后面会单独写一篇源码解析。大致流程就是,自动生成 requestCode,注册回调并存储起来,绑定生命周期,当收到Lifecycle.Event.ON_DESTROY事件时,自动解绑注册。
代替startActivityForResult()的就是ActivityResultLauncher.launch()方法,最后会调用到ActivityResultRegistry.invoke()方法,如下所示:
@Override public <I, O> void invoke( final int requestCode, @NonNull ActivityResultContract<I, O> contract, I input) { Intent intent = contract.createIntent(input); if (ACTION_REQUEST_PERMISSIONS.equals(intent.getAction())) { // handle request permissions } else { ComponentActivity.this.startActivityForResult(intent, requestCode); } } 复制代码
中间那一块处理 request permissions 的我给掐掉了。这样看起来看清晰。本来准备单独水一篇QQ号出售平台源码解析的,这马上核心源码都讲完了。
前面展示过了startActivityForResult(),再来展示一下权限请求。
private val requestPermission = prepareCall(ActivityResultContracts.RequestPermission()){ result -> toast("request permission $result") } requestPermission.launch(Manifest.permission.READ_PHONE_STATE) 复制代码
拨打电话,拍照就不在这里展示了。所有的示例代码都已经上传到了我的 Github 。
如何自定义返回值 ?
前面提到的都是系统预置的协议,返回值也都是固定的。那么,如何返回自定义类型的值呢?其实也很简单,自定义ActivityResultContract就可以了。
我们以TakePicture为例,默认的返回值是Bitmap,现在我们让它返回Drawable。
private class TakePicDrawable : ActivityResultContract<Void,Drawable>(){ override fun createIntent(input: Void?): Intent { return Intent(MediaStore.ACTION_IMAGE_CAPTURE) } override fun parseResult(resultCode: Int, intent: Intent?): Drawable? { if (resultCode != Activity.RESULT_OK || intent == null) return null val bitmap = intent.getParcelableExtra<Bitmap>("data") return BitmapDrawable(bitmap) } } 复制代码
使用:
private val takePictureCustom = prepareCall(TakePicDrawable()) { result -> toast("take picture : $result") } pictureCustomBt.setOnClickListener { takePictureCustom()} 复制代码
这样就可以调用系统相机拍照并在结果回调中拿到 Drawable 对象了。
说好的解耦呢 ?
有时候我们可能会在结果回调中进行一些复杂的处理操作,无论是之前的onActivityResult()还是上面的写法,都是直接耦合在视图控制器中的。通过新的 Activity Result API,我们还可以单独的类中处理结果回调,真正做到单一职责。
其实 Activity Result API 的核心操作都是通过ActivityResultRegistry来完成的,ComponentActivity 中包含了一个ActivityResultRegistry对象 :
@NonNull public ActivityResultRegistry getActivityResultRegistry() { return mActivityResultRegistry; } 复制代码
现在要脱离 Activity 完成操作,就需要外部提供一个ActivityResultRegistry对象来进行结果回调的注册工作。同时,我们一般通过实现LifecycleObserver接口,绑定一个LifecycleOwner来进行自动解绑注册。完整代码如下:
class TakePhotoObserver( private val registry: ActivityResultRegistry, private val func: (Bitmap) -> Unit ) : DefaultLifecycleObserver { private lateinit var takePhotoLauncher: ActivityResultLauncher<Void?> override fun onCreate(owner: LifecycleOwner) { takePhotoLauncher = registry.registerActivityResultCallback( "key", ActivityResultContracts.TakePicture() ) { bitmap -> func(bitmap) } } fun takePicture(){ takePhotoLauncher() } } 复制代码
再玩点花出来 ?
在 Github 上看到了一些花式写法,和大家分享一下。
class TakePhotoLiveData(private val registry: ActivityResultRegistry) : LiveData<Bitmap>() { private lateinit var takePhotoLauncher : ActivityResultLauncher<Intent> override fun onActive() { super.onActive() registry.registerActivityResultCallback("key", ActivityResultContracts.TakePicture()){ result -> value = result } } override fun onInactive() { super.onInactive() takePhotoLauncher.dispose() } } 复制代码
通过绑定 LiveData 自动注册和解绑。
最后
不知道你如何看待最新的 Activity Result API ,欢迎在评论区留下你的意见。
更多相关文章
- 代码审查常见问题,建议收藏
- 趣学Spring:一文搞懂Aware、异步编程、计划任务
- “百行代码”实现简单的Python分布式爬虫
- Docker社区核心成员Doug Davis分享为社区贡献代码的技巧
- 用MongoDB Change Streams 在BigQuery中复制数据
- Spring Boot 2.3 新特性分层JAR
- 实力解剖一枚挖矿脚本,风骚操作亮瞎双眼
- 一文读懂 TS 中 Object, object, {} 类型之间的区别
- Netfliter状态跟踪之动态协议的实现浅析(tftp实现)