本文已授权
[郭霖]
公众号独家发布
前言
之前写了两篇文章讲了 ViewBinding 的封装,这是 Jetpack 的一个组件,用于替代 findViewById、ButterKnife、KAE。不过用到了些 Kotlin 相对进阶点的用法,可能有些不太熟悉 Kotlin 的小伙伴看不太懂封装的代码。
所以这次来讲些简单一点的封装,来封装 Jetpack 的另一个组件——Activity Result API。这是官方用于替代 startActivityForResult()
和 onActivityResult()
的工具,能替代但是不够好用,有些小伙伴看了后还是选择写 startActivityForResult()
。需要封装优化一下用法,但是推出大半年了个人没看到比较好用的封装。最初有很多人会用拓展函数进行封装,而在 activity-ktx:1.2.0-beta02
版本之后,调用注册方法的时机必须在 onStart()
之前,原来的拓展函数就不适用了,在这之后就没看到有人封装了。
个人对 Activity Result API 的封装思考了很久,已经尽量做到在 Kotlin 和 Java 都足够地好用,可以完美替代 startActivityForResult()
了。下面带着大家一起来封装 Activity Result API。
基础用法
首先要了解 Activity Result API 的用法。
添加依赖:
dependencies {
implementation "androidx.activity:activity-ktx:1.2.4"
}
在 ComponentActivity
或 Fragment
中调用 Activity Result API 提供的 registerForActivityResult()
方法注册结果回调(在 onStart() 之前调用)。该方法接收 ActivityResultContract
和 ActivityResultCallback
参数,返回可以启动另一个 activity 的 ActivityResultLauncher
对象。
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
ActivityResultContract
协议类定义生成结果所需的输入类型以及结果的输出类型,Activity Result API 已经提供了很多默认的协议类,方便大家实现请求权限、拍照等常见操作。
只是注册回调并不会启动另一个 activity ,还要调用 ActivityResultLauncher#launch()
方法才会启动。传入协议类定义的输入参数,当用户完成后续 activity 的操作并返回时,将执行 ActivityResultCallback
中的 onActivityResult()
回调方法。
getContent.launch("image/*")
完整的使用代码:
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
selectButton.setOnClickListener {
getContent.launch("image/*")
}
}
ActivityResultContracts
提供了许多默认的协议类:
协议类 | 作用 |
---|---|
StartActivityForResult() | 通用协议 |
TakePicturePreview() | 拍照预览,返回 Bitmap |
TakePicture() | 拍照,返回 Uri |
TakeVideo() | 录像,返回 Uri |
GetContent() | 获取单个内容文件 |
GetMultipleContents() | 获取多个内容文件 |
RequestPermission() | 请求单个权限 |
RequestMultiplePermissions() | 请求多个权限 |
CreateDocument() | 创建文档 |
OpenDocument() | 打开单个文档 |
OpenMultipleDocuments() | 打开多个文档 |
OpenDocumentTree() | 打开文档目录 |
PickContact() | 选择联系人 |
我们还可以自定义协议类,继承 ActivityResultContract
,定义输入和输出类。如果不需要任何输入,可使用 Void
或 Unit
作为输入类型。需要实现两个方法,用于创建与 startActivityForResult()
配合使用的 Intent
和解析输出的结果。
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)
}
}
自定义协议类实现后,就能调用 registerForActivityResult()
和 launch()
方法进行使用。
val pickRingtone = registerForActivityResult(PickRingtone()) { uri: Uri? ->
// Handle the returned Uri
}
pickRingtone.launch(ringtoneType)
不想自定义协议类的话,可以使用通用的协议 ActivityResultContracts.StartActivityForResult()
,实现类似于之前 startActivityForResult()
的功能。
val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.intent
// Handle the Intent
}
}
startForResult.launch(Intent(this, InputTextActivity::class.java))
封装思路
为什么要封装?
看完上面的用法,不知道大家会不会和我初次了解的时候一样,感觉比原来复杂很多。
主要是引入的新概念比较多,原来只需要了解 startActivityForResult()
和 onActivityResult()
的用法,现在要了解一大堆类是做什么的,学习成本高了不少。
用法也有些奇怪,比如官方示例用注册方法得到一个叫 getContent
对象,这更像是函数的命名,还要用这个对象去调用 launch()
方法,代码阅读起来总感觉怪怪的。
而且有个地方个人觉得不是很好,callback 居然在 registerForActivityResult()
方法里传。个人觉得 callback 在 launch()
方法里传更符合习惯,逻辑也更加连贯,代码阅读性更好。最好改成下面的用法,启动后就接着处理结果的逻辑。
getContent.launch("image/*") { uri: Uri? ->
// Handle the returned Uri
}
所以还是有必要对 Activity Result API 进行封装的。
怎么封装?
首先是修改 callback 传参的位置,实现思路也比较简单,重载 launch() 方法加一个 callback 参数,用个变量缓存起来。在回调的时候拿缓存的 callback 对象去执行。
private var callback: ActivityResultCallback<O>? = null
fun launch(input: I?, callback: ActivityResultCallback<O>) {
this.callback = callback
launcher.launch(input)
}
由于需要缓存 callback 对象,还要写一个类来持有该缓存变量。
有一个不好处理的问题是 registerForActivityResult()
需要的 onStart() 之前调用。可以通过 lifecycle 在 onCreate() 的时候自动注册,但是个人思考了好久并没有想到更优的实现方式。就是获取 lifecycleOwner 观察声明周期自动注册,也是需要在 onStart() 之前调用,那为什么不直接执行注册方法呢?所以个人改变了思路,不纠结于自动注册,而是简化注册的代码。
前面说了需要再写一个类缓存 callback 对象,使用一个类的时候有个方法基本会用到,就是构造函数。我们可以在创建对象的时候进行注册。
注册方法需要 callback 和协议类对象两个参数,callback 是从 launch()
方法得到,而协议类对象就需要传了。这样用起来个人觉得还不够友好,综合考虑后决定用继承的方式把协议类对象给“隐藏”了。
最终得到以下的基类。
public class BaseActivityResultLauncher<I, O> {
private final ActivityResultLauncher<I> launcher;
private ActivityResultCallback<O> callback;
public BaseActivityResultLauncher(ActivityResultCaller caller, ActivityResultContract<I, O> contract) {
launcher = caller.registerForActivityResult(contract, (result) -> {
if (callback != null) {
callback.onActivityResult(result);
callback = null;
}
});
}
public void launch(@SuppressLint("UnknownNullness") I input, @NonNull ActivityResultCallback<O> callback) {
this.callback = callback;
launcher.launch(input);
}
}
改用了 Java 代码来实现,返回的结果可以判空也可以不判空,比如返回数组的时候一定不为空,只是数组大小为 0 。用 Kotlin 实现的话要写两个不同名的方法来应对这个情况,使用起来并不是很方便。
这是多增加一个封装的步骤来简化后续的使用,原本只是继承 ActivityResultContract
实现协议类,现在还需要再写一个启动器类继承 BaseActivityResultLauncher
。
比如用前面获取图片的示例,我们再封装一个 GetContentLauncher
类。
class GetContentLauncher(caller: ActivityResultCaller) :
BaseActivityResultLauncher<String, Uri>(caller, GetContent())
只需这么简单的继承封装,后续使用就更加简洁易用了。
val getContentLauncher = GetContentLauncher(this)
override fun onCreate(savedInstanceState: Bundle?) {
// ...
selectButton.setOnClickListener {
getContentLauncher.launch("image/*") { uri: Uri? ->
// Handle the returned Uri
}
}
}
再封装一个 Launcher 类的好处是,能更方便地重载 launch()
方法,比如在类里增加一个方法在获取图片之前会先授权读取权限。如果改用 Kotlin 拓展函数来实现,在 Java 会更加难用。Launcher 类能对 Java 用法进行兼顾。
我们还能支持 Kotlin 协程, 只需再写一个 BaseActivityResultLauncher
的拓展函数,在开头使用 suspend
修饰,使用 suspendCancellableCoroutine
将 launch()
方法转为协程的挂起方法。
suspend fun <I, O> BaseActivityResultLauncher<I, O>.launchForResult(input: I?) =
suspendCancellableCoroutine<O> { continuation ->
launch(input) {
if (it != null) {
continuation.resume(it)
} else {
continuation.cancel()
}
}
}
然后就能在协程作用域内,使用类似同步的方式写异步代码。
lifecycleScope.launch {
val uri = getContentLauncher.launchForResult("image/*")
// Handle the returned Uri
}
最后总结一下,对比原本 Activity Result API 的用法,改善了什么问题:
- 简化冗长的注册代码,改成简单地创建一个对象;
- 改善对象的命名,比如官方示例命名为
getContent
对象就很奇怪,这通常是函数的命名。优化后很自然地用类名来命名为getContentLauncher
,使用一个启动器对象调用launch()
方法会更加合理; - 改变回调的位置,使其更加符合使用习惯,逻辑更加连贯,代码阅读性更好;
- 输入参数和输出参数不会限制为一个对象,可以重载方法简化用法;
- 能更方便地整合多个启动器的功能,比如获取读取权限后再跳转相册选择图片;
- 能更好地支持协程;
最终用法
由于 Activity Result API 已有很多的协议类,如果每一个协议都去封装一个启动器类会有点麻烦,所以个人已经写好一个库 ActivityResultLauncher 方便大家使用。还新增和完善了一些功能,有以下特点:
- 完美替代
startActivityForResult()
- 支持 Kotlin 和 Java 用法
- 支持协程用法
- 支持拍照(已适配 Android 10)
- 支持录像(已适配 Android 10)
- 支持选择图片或视频
- 支持裁剪图片(已适配 Android 11)
- 支持请求权限
- 支持打开蓝牙
- 支持打开定位
- 支持使用存储访问框架 SAF
- 支持选择联系人
个人写了个 Demo 给大家来演示有什么功能,完整的代码在 Github 里。
下面来介绍 Kotlin 的用法,Java 的用法可以查看 Wiki 文档。
在根目录的 build.gradle 添加:
allprojects {
repositories {
// ...
maven { url 'https://www.jitpack.io' }
}
}
添加依赖:
dependencies {
implementation 'com.github.DylanCaiCoding:ActivityResultLauncher:1.1.0
}
用法也只有简单的两步:
第一步,在 ComponentActivity
或 Fragment
创建对应的对象,需要注意创建对象的时机要在 onStart()
之前。例如创建通用的启动器:
private val startActivityLauncher = StartActivityLauncher(this)
提供以下默认的启动器类:
启动器 | 作用 |
---|---|
StartActivityLauncher | 完美替代 startActivityForResult() |
TakePicturePreviewLauncher | 拍照预览,返回 bitmap |
TakePictureLauncher | 拍照,返回 uri,已适配 Android 10 |
TakeVideoLauncher | 录像,返回 uri,已适配 Android 10 |
PickContentLauncher, GetContentLauncher | 选择单个图片或视频 |
GetMultipleContentsLauncher | 选择多个图片或视频 |
CropPictureLauncher | 裁剪图片,已适配 Android 11 |
RequestPermissionLauncher | 请求单个权限 |
RequestMultiplePermissionsLauncher | 请求多个权限 |
AppDetailsSettingsLauncher | 打开系统设置的 App 详情页 |
EnableBluetoothLauncher | 打开蓝牙 |
EnableLocationLauncher | 打开定位 |
CreateDocumentLauncher | 创建文档 |
OpenDocumentLauncher | 打开单个文档 |
OpenMultipleDocumentsLauncher | 打开多个文档 |
OpenDocumentTreeLauncher | 访问目录内容 |
PickContactLauncher | 选择联系人 |
StartIntentSenderLauncher | 替代 startIntentSender() |
第二步,调用启动器对象的 launch()
方法。
比如跳转一个输入文字的页面,点击保存按钮回调结果。
我们用 StartActivityLauncher
替换掉原来 startActivityForResult()
的写法。
val intent = Intent(this, InputTextActivity::class.java)
intent.putExtra(KEY_NAME, "nickname")
startActivityLauncher.launch(intent) { activityResult ->
if (activityResult.resultCode == RESULT_OK) {
activityResult.data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
}
}
为了方便使用,有些启动器会增加一些更易用的 launch()
方法。比如这个例子能改成下面更简洁的写法。
startActivityLauncher.launch<InputTextActivity>(KEY_NAME to "nickname") { resultCode, data ->
if (resultCode == RESULT_OK) {
data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
}
}
由于输入文字页面可能有多个地方需要跳转复用,我们可以用前面的封装思路,自定义实现一个 InputTextLauncher
类,进一步简化调用的代码,只关心输入值和输出值,不用再处理跳转和解析过程。
inputTextLauncher.launch("nickname") { value ->
if (value != null) {
toast(value)
}
}
通常要对返回值进行判断,因为可能会有取消操作,要判断是不是被取消了。比如返回的 Boolean 要为 true,返回的 Uri 不为 null,返回的数组不为空数组等。
还有一些常用的功能,比如调用系统相机拍照,保存到外置存储的应用缓存目录。
takePictureLauncher.launch { uri ->
if (uri != null) {
// 拍照成功,上传或取消等操作后建议把缓存文件删除
}
}
或者拍照保存到系统相册,已适配 Android 10。
takePictureLauncher.launchForMediaImage { uri ->
if (uri != null) {
// 拍照成功
}
}
调用系统相册选择图片,增加了申请读取权限的操作。
pickContentLauncher.launchForImage(
onActivityResult = { uri ->
if (uri != null) {
// 处理 uri
}
},
onPermissionDenied = {
// 拒绝了读取权限且不再询问,可引导用户到设置里授权该权限
},
onExplainRequestPermission = {
// 拒绝了一次读取权限,可弹框解释为什么要获取该权限
}
)
能更方便地使用存储访问框架 SAF,在 Android 10 或以上访问共享储存空间的文档文件会用到。
个人也新增了些功能,比如裁剪图片,通常上传头像要裁剪成 1:1 比例,已适配 Android 11。
cropPictureLauncher.launch(inputUri) { uri ->
if (uri != null) {
// 裁剪成功
}
}
还有开启蓝牙功能,能更容易地开启蓝牙并且确保蓝牙功能是可用的(需要授权定位权限和确保定位已打开)。
enableBluetoothLauncher.launchAndEnableLocation(
"为保证蓝牙正常使用,请开启定位", // 已授权权限但未开启定位,会跳转对应设置页面,并吐司该字符串
onLocationEnabled= { enabled ->
if (enabled) {
// 已开启了蓝牙,并且授权了位置权限和打开了定位
}
},
onPermissionDenied = {
// 拒绝了位置权限且不再询问,可引导用户到设置里授权该权限
},
onExplainRequestPermission = {
// 拒绝了一次位置权限,可弹框解释为什么要获取该权限
}
)
对 Kotlin 协程熟悉的小伙伴还可以选择协程的用法,使用类似同步的方式写异步代码。在协程作用域内调用 launchForResult()
得到返回的结果,或者调用 launchForFlow()
得到数据流进行流式编程。
下面给出一段运用 Kotlin Flow 流式编程的示例。
// 第一步,请求读取 SD 卡权限
requestPermissionLauncher.launchForFlow(Manifest.permission.READ_EXTERNAL_STORAGE,
onDenied = {
showDialog(...)
},
onExplainRequest = {
showDialog(...)
})
.transform { emit(pickContentLauncher.launchForImageResult()) } // 第二步,选择相册图片
.transform { emit(cropPictureLauncher.launchForResult(it)) } // 第三步,裁剪图片
.collect { uri ->
// 第四步,上传头像
}
多个操作会产生多个回调,使用流式编程能够很好地避免回调地狱,减少代码的层级,代码可读性会更好。
更多的用法请查看 Wiki 文档 。如果有其它使用场景或者别的想法可以在 Github 提 issue,我会继续完善的。
原本 Activity Result API 已经有很多默认的协议类,都封装了对应的启动器类。大家可能不会用到所有类,不过开了混淆会自动移除没有使用的类。
彩蛋
个人之前封装过一个 startActivityForResult()
拓展函数,可以直接在后面写回调逻辑。
startActivityForResult(intent, requestCode) { resultCode, data ->
// Handle result
}
下面是实现的代码,使用一个 Fragment 来分发 onActivityResult 的结果。代码量不多,逻辑应该比较清晰,感兴趣的可以了解一下,Activity Result API 的实现原理应该也是类似的。
inline fun FragmentActivity.startActivityForResult( intent: Intent, requestCode: Int, noinline callback: (resultCode: Int, data: Intent?) -> Unit ) =
DispatchResultFragment.getInstance(this).startActivityForResult(intent, requestCode, callback)
class DispatchResultFragment : Fragment() {
private val callbacks = SparseArray<(resultCode: Int, data: Intent?) -> Unit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
fun startActivityForResult( intent: Intent, requestCode: Int, callback: (resultCode: Int, data: Intent?) -> Unit ) {
callbacks.put(requestCode, callback)
startActivityForResult(intent, requestCode)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val callback = callbacks.get(requestCode)
if (callback != null) {
callback.invoke(resultCode, data)
callbacks.remove(requestCode)
}
}
companion object {
private const val TAG = "dispatch_result"
fun getInstance(activity: FragmentActivity): DispatchResultFragment {
val fragmentManager = activity.supportFragmentManager
var fragment = fragmentManager.findFragmentByTag(TAG) as DispatchResultFragment?
if (fragment == null) {
fragment = DispatchResultFragment()
fragmentManager.beginTransaction().add(fragment, TAG).commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
}
return fragment
}
}
}
如果觉得 Activity Result API 比较复杂,也可以拷贝这个去用。不过 requestCode 处理得不够好,而且很多功能需要自己额外去实现,用起来没那么方便。
往期讲解封装的文章
总结
本文讲了 Activity Result API 的基础用法,虽然能替代 startActivityForResult()
和 onActivityResult()
,但没有做到足够地好用,有些人还宁愿继续使用 startActivityForResult()
。之后分享了个人的封装思路,介绍了个人封装的库 ActivityResultLauncher,使 Activity Result API 更加简洁易用,能完美地替代 startActivityForResult()
。
如果您觉得有帮助的话,希望能点个 star 支持一下哟 ~ 我后面会分享更多封装相关的文章给大家。
今天的文章优雅地封装 Activity Result API,完美地替代 startActivityForResult()分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/13614.html