ActivityResultLauncher 是 Android Activity Result API 的一个组件,在 androidx.activity 库的 Activity 1.2.0 版本中引入。它取代了自 Android SDK 创建以来就存在的过时方法 startActivityForResult 和 onActivityResult。根据 Android Developers (2024),新API消除了与Activity的紧密耦合和缺乏类型安全的问题。ActivityResultLauncher 提前注册并使用 Contract 对输入和输出数据进行严格类型化。
要点
ActivityResultLauncher 是来自 androidx.activity.result 包的一个类,提供了一种类型安全的机制来启动Activity并获取结果。Launcher 通过 registerForActivityResult 方法创建,该方法接受两个参数:Contract(描述输入和输出类型)和 ActivityResultCallback(结果处理器)。注册后,launcher 即可通过 launch 方法进行调用。
与旧API相比的关键区别在于注册和启动的分离。注册在初始化阶段(Activity.onCreate 或 Fragment.onCreate)完成,callback 与 launcher 绑定一次,并在结果返回时保证触发。这解决了 onActivityResult 以意外顺序或在已销毁的Activity上触发的问题。
ActivityResultLauncher 支持所有以前通过 onActivityResult 处理的场景:启动相机、图库、请求联系人、权限和自定义Activity。此外,API是可扩展的:开发人员可以为Activity之间数据交换的特定场景创建自定义Contract。
startActivityForResult 自 API Level 1(2008年)以来一直是 Android SDK 的一部分,并在超过12年的时间里一直是获取Activity结果的主要方式。然而,这种方法存在 Google 在 Activity Result API 中解决的根本性缺陷。让我们看看主要问题以及新API如何解决它们。
startActivityForResult 方法通过 requestCode(一个传递给 onActivityResult 的任意整数)与 Activity 和 Fragment 关联。开发人员手动将代码与启动的操作匹配,这导致代码重用和继承时出现错误。ActivityResultLauncher 完全消除了 requestCode:callback 在注册阶段绑定到特定的 launcher,并且仅为其调用。
在配置更改(屏幕旋转、语言更改)时,Activity 被重新创建,onActivityResult 可能无法触发——callback 丢失。Activity Result API 通过 SavedStateRegistry 自动保存和恢复 launcher 状态,确保即使在 Activity 重新创建后也能收到结果。
旧API通过带有 Bundle 的 Intent 传递结果,其中的键和数据类型不受编译器检查。Activity Result API 使用 Contract——一个通用接口,定义输入数据类型(I)和结果类型(O)。类型不匹配错误在编译阶段被检测到,而不是在运行时。
| 特性 | startActivityForResult | ActivityResultLauncher |
|---|---|---|
| RequestCode | 需要手动管理 | 自动,不需要 |
| 类型安全 | 无 | 通用 Contract |
| 旋转时保存 | 丢失 | SavedStateRegistry |
| 最低API | API Level 1 | Activity 1.2.0 |
| 在Compose中使用 | 不支持 | rememberLauncherForActivityResult |
Contract 是一个 ActivityResultContract<I, O> 接口,定义了如何启动 Activity 以及如何解释结果。Google 提供了一组内置约定,覆盖了开发人员的大部分需求。
StartIntentSenderForResult 是启动 IntentSender 的基本约定。用于系统场景,例如通过 Google Sign-In 进行身份验证或通过 Google Pay 进行支付。输入参数——PendingIntent,输出——带有代码和 Intent 的 ActivityResult。
RequestMultiplePermissions 是在 Android 6.0+ 中同时请求多个权限的约定。输入参数——包含权限名称的 String 数组,输出——包含每个请求结果的 Map<String, Boolean>。以前这需要在 onRequestPermissionsResult 中手动解析并匹配请求代码。
TakePicture 是通过系统相机拍照的约定。输入——保存照片的 Uri,输出——Boolean(成功)。TakeVideo 类似地处理视频。这些约定取代了过时的 MediaStore.ACTION_IMAGE_CAPTURE,后者在不同设备上行为不稳定。
GetContent 是通过系统选择器选择内容的约定。输入——MIME类型(例如 image/*),输出——所选文件的 Uri。OpenDocument 的区别在于支持多选和按文档类型过滤。两个约定都通过 SAF(Storage Access Framework)工作。
CreateDocument 是通过系统对话框创建新文档的约定。用户选择名称和文件夹,系统返回用于写入的 Uri。OpenDocumentTree 提供对整个目录的访问——用户选择一个文件夹,应用程序获取用于读取和写入其中所有文件的 tree-uri。
在经典Android中使用ActivityResultLauncher的基本模式包括两个步骤:在初始化阶段通过 registerForActivityResult 注册,以及响应用户操作调用 launch。让我们看一个从图库中选择图像的典型示例。
在 Activity 的 onCreate 中注册launcher——这确保在任何可能的调用之前 callback 已准备就绪。切勿在启动前立即注册 launcher——这违反了 API 约定,并可能导致 Activity 重新创建时结果丢失。
class MainActivity : AppCompatActivity() {
private val pickImageLauncher =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
uri?.let { binding.imageView.setImageURI(it) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pickImageLauncher.launch("image/*")
}
}
在Fragment中,注册在 onCreate、onAttach 或 onCreateView 中的初始化时完成。FragmentActivity 通过父级 Activity 传递 launcher,因此结果在 Fragment 内部处理,而不是在 Activity 中。与 onActivityResult 相比,这改进了封装,在 onActivityResult 中,所有 Fragment 的所有结果都集中在一个 Activity 方法中。
class ProfileFragment : Fragment() {
private val cameraLauncher =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) { updateProfilePhoto() }
}
fun takePhoto(photoUri: Uri) {
cameraLauncher.launch(photoUri)
}
}
Jetpack Compose 为 Activity Result API 提供了特殊的 composable 功能——rememberLauncherForActivityResult。与经典方法不同,在 Compose 中,launcher 是通过 remember 绑定到 composable 生命周期的对象创建的。这使得可以在无需直接访问 Activity 或 Fragment 的情况下,以完全声明式的方式使用 Activity Result API。
rememberLauncherForActivityResult 接受 Contract 和 callback,返回 ActivityResultLauncher。launcher 在重组期间保留,并在退出组合时自动清理。调用 launch 作为对事件的响应发生——例如按钮点击或状态更改。
@Composable
fun PhotoPicker() {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) { uri -> handleImage(uri) }
Button(onClick = { launcher.launch("image/*") }) {
Text("选择照片")
}
}
在 Compose 中请求权限也通过 rememberLauncherForActivityResult 使用 RequestPermission 或 RequestMultiplePermissions 约定完成。Google 建议使用 accompanist-permissions,但其底层也使用 Activity Result API。为了方便控制权限状态,可以将状态存储在 remember 或 ViewModel 中。
Activity Result API 消除了旧方法的许多问题,但错误使用可能导致新类型的错误。让我们看看最常见的问题以及如何避免它们。
launcher 的注册必须在组件初始化时完成——在 Activity 的 onCreate 或 Fragment 的初始化器中。如果在 lambda、callback 或协程内部注册 launcher,Activity 重新创建时注册可能会重复执行,旧 launcher 将失去与结果的联系。
每个 launcher 获得一个唯一的键来保存状态。如果在同一组件中使用相同的 Contract 注册两个 launcher,SavedStateRegistry 可能会覆盖其中一个的状态。Android Studio 通过 UnnecessaryRegisterForActivityResult lint 规则对此发出警告,但最好手动控制唯一性。
用户可以取消操作——按下系统返回键、最小化应用程序或切换到其他应用程序。在这种情况下,callback 将收到 null 或带有 RESULT_CANCELED 的 ActivityResult。使用前始终检查结果是否为 null,以避免 NullPointerException。
如果您的应用程序经常启动类似场景——例如选择联系人并返回姓名和电话——请创建自己的 Contract。这提高了代码的可读性,并允许集中更改启动和结果处理的逻辑。
class PickContactContract : ActivityResultContract<Void, ContactData?>() {
override fun createIntent(context: Context, input: Void?) =
Intent(Intent.ACTION_PICK).setType(ContactsContract.Contacts.CONTENT_TYPE)
override fun parseResult(resultCode: Int, intent: Intent?) =
intent?.data?.let { queryContact(it) }
}
常见问题
不能 — ActivityResultLauncher 需要 Activity 或 Fragment 上下文进行注册。ViewModel 仅用于存储状态,launcher 在 Activity 或 Fragment 中创建,并将结果传递给 ViewModel。
Activity Result API 从 activity-ktx 1.2.0 库开始可用。最低 SDK — API Level 14(Android 4.0),但大多数约定仅在 API Level 19+ 上工作。
重复调用 launch 在第一个操作完成之前将被忽略。Activity Result API 不支持并行启动——在新调用之前等待第一个操作的 callback。
迁移通过将 startActivityForResult 调用替换为 registerForActivityResult 并使用相应的 Contract 来完成。删除 onActivityResult 并在 launcher 的 callback 中处理结果。Google 在 Android Developers 文档中提供了迁移指南。
可以,许多库支持通过 ActivityResultContracts 进行集成。例如,ML Kit Barcode Scanner 使用 StartIntentSenderForResult 来启动扫描器。请查看具体库的文档。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。