为什么要丢掉 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 的我给掐掉了。这样看起来看清晰。本来准备单独水一篇源码解析的,这马上核心源码都讲完了。
前面展示过了 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 ,欢迎在评论区留下你的意见。