启动另一个 Activity(无论是您应用中的 Activity 还是其他应用中的 Activity)不一定是单向操作。您也可以启动另一个 Activity 并接收返回的结果。例如,您的应用可启动相机应用并接收拍摄的照片作为结果。或者,您可以启动“通讯录”应用以便用户选择联系人,并且您将接收联系人详细信息作为结果。
虽然所有 API 级别的 Activity 类均提供底层 startActivityForResult() 和 onActivityResult() API,但我们强烈建议您使用 AndroidX Activity 1.2.0-alpha02 和 Fragment 1.3.0-alpha02 中引入的 Activity Result API。
Activity Result API 提供了用于注册结果、启动结果以及在系统分派结果后对其进行处理的组件。
在启动 Activity 以获取结果时,可能会出现您的进程和 Activity 因内存不足而被销毁的情况;如果是使用相机等内存密集型操作,几乎可以确定会出现这种情况。
因此,Activity Result API 会将结果回调从您之前启动另一个 Activity 的代码位置分离开来。由于在重新创建进程和 Activity 时需要使用结果回调,因此每次创建 Activity 时都必须无条件注册回调,即使启动另一个 Activity 的逻辑仅基于用户输入内容或其他业务逻辑也是如此。
位于 ComponentActivity 或 Fragment 中时,Activity Result API 会提供 prepareCall() API,用于注册结果回调。prepareCall() 接受 ActivityResultContract 和 ActivityResultCallback 作为参数,并返回 ActivityResultLauncher,供您用来启动另一个 Activity。
ActivityResultContract 定义生成结果所需的输入类型以及结果的输出类型。这些 API 可为拍照和请求权限等基本 intent 操作提供默认协定。您还可以创建自己的自定义协定。
ActivityResultCallback 是单一方法接口,带有 onActivityResult() 方法,可接受 ActivityResultContract 中定义的输出类型的对象:
val getContent = prepareCall(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
如果您有多个使用不同协定或需要单独回调的 Activity 结果调用,则您可以多次调用 prepareCall(),以准备多个 ActivityResultLauncher 实例。每次创建 Fragment 或 Activity 时,都必须按照相同的顺序调用 prepareCall(),才能确保将生成的结果传递给正确的回调。
在 Fragment 或 Activity 创建完毕之前可安全地调用 prepareCall(),因此,在为返回的 ActivityResultLauncher 实例声明成员变量时可以直接使用它。
注意:虽然在 Fragment 或 Activity 创建完毕之前可安全地调用 prepareCall(),但在 Fragment 或 Activity 的 Lifecycle 变为 CREATED 状态之前,您无法启动 ActivityResultLauncher。
虽然 prepareCall() 会注册您的回调,但它不会启动另一个 Activity 并发出结果请求。这些操作由返回的 ActivityResultLauncher 实例负责。
如果存在输入内容,则启动器接受与 ActivityResultContract 的类型匹配的输入内容。调用 launch() 会启动生成结果的过程。当用户完成后续 Activity 并返回时,系统将执行 ActivityResultCallback 中的 onActivityResult(),如以下示例所示:
val getContent = prepareCall(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?) {
// ...
val selectButton = findViewById<Button>(R.id.select_button)
selectButton.setOnClickListener {
// Use the Kotlin extension in activity-ktx
// passing it the mime type you'd like to allow the user to select
getContent("image/*")
}
}
除了传递输入内容之外,launch() 的重载版本还允许您传递 ActivityOptionsCompat。
注意:由于在调用 launch() 与触发 onActivityResult() 回调的两个时间点之间,您的进程和 Activity 可能会被销毁,因此,处理结果所需的任何其他状态都必须与这些 API 分开保存和恢复。
虽然 ComponentActivity 和 Fragment 类通过实现 ActivityResultCaller 接口来允许您使用 prepareCall() API,但您也可以直接使用 ActivityResultRegistry 在未实现 ActivityResultCaller 的单独类中接收结果。
例如,您可能需要实现一个 LifecycleObserver,用于处理协定的注册和启动器的启动:
class MyLifecycleObserver(private val registry : ActivityResultRegistry)
: DefaultLifecycleObserver {
lateinit var getContent : ActivityResultLauncher<String>
fun onCreate(owner: LifecycleOwner) {
getContent = registry.register("key", owner, GetContent()) { uri ->
// Handle the returned Uri
}
}
fun selectImage() {
getContent("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<Button>(R.id.select_button)
selectButton.setOnClickListener {
// Open the activity to select an image
observer.selectImage()
}
}
}
使用 ActivityResultRegistry API 时,强烈建议您使用可接受 LifecycleOwner 作为参数的 API,因为 LifecycleOwner 会在 Lifecycle 被销毁时自动移除已注册的启动器。不过,如果 LifecycleOwner 不存在,则每个 ActivityResultLauncher 类都允许您手动调用 unregister() 作为替代。
默认情况下,prepareCall() 会自动使用 Activity 提供的 ActivityResultRegistry。此外,它还提供了一个重载,让您可以传入自己的 ActivityResultRegistry 实例。该实例可用于测试您的 Activity 结果调用,无需实际启动另一个 Activity。
测试应用的 Fragment 时,使用 FragmentFactory 将 ActivityResultRegistry 传入该 Fragment 的构造函数即可提供测试 ActivityResultRegistry。
注意:任何允许您在测试中注入单独 ActivityResultRegistry 的机制都足以支持您测试 Activity 结果调用。
例如,使用 TakePicturePreview 协定获取图片缩略图的 Fragment 可能按类似如下所示的方式编写:
class MyFragment(
private val registry: ActivityResultRegistry
) : Fragment() {
val thumbnailLiveData = MutableLiveData<Bitmap?>
val takePicture = prepareCall(TakePicturePreview(), registry) {
bitmap: Bitmap? -> thumbnailLiveData.setValue(bitmap)
}
// ...
}
创建专用于测试的 ActivityResultRegistry 时,必须实现 invoke() 方法。您的测试实现可以直接调用 dispatchResult(),而不是调用 startActivityForResult(),从而提供要在测试中使用的确切结果:
val testRegistry = object : ActivityResultRegistry() {
override fun <I, O> invoke(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
dispatchResult(requestCode, expectedResult)
}
}
完整的测试会产生预期的结果,构造一个测试 ActivityResultRegistry,将其传递给 Fragment,直接触发启动器或通过 Espresso 等其他测试 API 触发启动器,然后验证结果:
@Test
fun activityResultTest {
// Create an expected result Bitmap
val expectedResult = Bitmap.createBitmap(1, 1, Bitmap.Config.RGBA_F16)
// Create the test ActivityResultRegistry
val testRegistry = object : ActivityResultRegistry() {
override fun <I, O> invoke(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
dispatchResult(requestCode, expectedResult)
}
}
// Use the launchFragmentInContainer method that takes a
// lambda to construct the Fragment with the testRegistry
with(launchFragmentInContainer { MyFragment(testRegistry) }) {
onFragment { fragment ->
// Trigger the ActivityResultLauncher
fragment.takePicture()
// Verify the result is set
assertThat(fragment.thumbnailLiveData.value)
.isSameInstanceAs(expectedResult)
}
}
}
虽然 ActivityResultContracts 包含一些预先构建的可用 ActivityResultContract 类,但您可以使用自己的协定,提供您所需要的精确类型安全 API。
每个 ActivityResultContract 都需要定义输入和输出类,如果您不需要任何输入,可使用 Void(在 Kotlin 中,使用 Void? 或 Unit)作为输入类型。
每个协定都必须实现 createIntent() 方法,该方法接受 Context 和输入内容作为参数,并构造将与 startActivityForResult() 配合使用的 Intent。
每个协定还必须实现 parseResult(),这会根据指定的 resultCode(如 Activity.RESULT_OK 或 Activity.RESULT_CANCELED)和 Intent 生成输出内容。
如果无需调用 createIntent()、启动另一个 Activity 并借助 parseResult() 来构建结果即可确定指定输入内容的结果,则协定可以选择性地实现 getSynchronousResult()。
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)
}
}
如果您不需要自定义协定,则可以使用StartActivityForResult协定。这是一个通用协定,它可接受任何 Intent 作为输入内容并返回 ActivityResult,让您能够在回调中提取 resultCode 和 Intent,如以下示例所示:
val startForResult = prepareCall(StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.intent
// Handle the Intent
}
}
override fun onCreate(savedInstanceState: Bundle) {
// ...
val startButton = findViewById(R.id.start_button)
startButton.setOnClickListener {
// Use the Kotlin extension in activity-ktx
// passing it the Intent you want to start
startForResult(Intent(this, ResultProducingActivity::class.java))
}
}