之前在 Jetpack Compose中的导航路由 里简单的提到了从 Compose 导航到其他 Activity 页面的方式,对于不带返回结果的则就是跟以前一样简单的启动Activity的代码,而如果是startActivityForResult方式的,需要使用带回调的方式去启动,那么在以前,我们要么是使用三方库,要么是自己封装一个简单的库来使用 (至于背后原理也不是什么新鲜事了) 。

后来研究了一下,在Compose 中startActivityForResult也有了新的姿势,要理解Compose 中startActivityForResult的姿势,这还得从androidxstartActivityForResult的姿势说起,因为Compose 就是在androidx的基础上利用其API简单封装了一下而已。

倒也不是说以前的不能用了,毕竟有系统自带的,谁还去用第三方的呀,用第三方的还得导入一个依赖库不是,能少依赖三方的就尽量少依赖吧。

androidx之后如何正确的startActivityForResult

如何使用

如果是在ActivityFragment内部使用的话,直接调用registerForActivityResult方法即可。

例如,选择文件:

1
2
3
4
5
val launcher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.apply { showToast(uri.toString()) }
}

launcher.launch("image/*")

ActivityResultContracts.GetContent()的方式获取文件launch时需要通过指定 mime type来过滤文件类型, 例如 image/* ,这会打开系统自带的一个文件选择器供你选择文件。

录制视频:

1
2
3
4
5
6
7
8
9
val outVideoFile = File(externalCacheDir, "/${System.currentTimeMillis()}.mp4")
val videoUri = FileProvider.getUriForFile(this, "${packageName}.provider", outVideoFile)
val launcher = registerForActivityResult(ActivityResultContracts.CaptureVideo()) { isSuccess ->
if (isSuccess) {
showToast(outVideoFile.path)
}
}

launcher.launch(videoUri)

拍照:

1
2
3
4
5
6
7
8
9
val outPictureFile = File(externalCacheDir, "/${System.currentTimeMillis()}.jpeg")
val pictureUri = FileProvider.getUriForFile(this, "${packageName}.provider", outPictureFile)
val launcher = registerForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
if (isSuccess) {
showToast(outPictureFile.path)
}
}

launcher.launch(pictureUri)}

这两种方式需要指定Uri, 这个Uri获取有点费劲,需要先进行FileProvider配置,不过配置好就很方便了。(如果你的minSdk配置的是29,那么需要另外配置,可自行查阅相关资料,不过国内基本不会兼容这么高的版本,一般minSdk配置的会是21)

自己的内部业务Activity之间的跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
// MainActivity.kt 
val target = Intent(this, OtherActivity::class.java).apply {
putExtra("name", "张三")
putExtra("uid", 123)
}
val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
activityResult.data?.apply {
val name = getStringExtra("name")
name?.let { showToast(it) }
}
}

launcher.launch(target)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// OtherActivity.kt
class OtherActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val name = intent.getStringExtra("name")
val uid = intent.getIntExtra("uid", -1)
setContent {
MyComposeApplicationTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column {
Text("name: $name fromCompose: $fromCompose uid: $uid", fontSize = 20.sp)
Button(onClick = {
val data = Intent().apply { putExtra("name", "小明") }
setResult(RESULT_OK, data)
finish()
}) { Text("back with result") }
}
}
}
}
}
}

registerForActivityResult的回调接口lambda中,仍然可以像以前那样使用activityResult.resultCode == RESULT_OK 来判断是正常返回了,还是取消操作。

在Activity/Fragment以外的类中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyLifecycleObserver(private val registry : ActivityResultRegistry) : DefaultLifecycleObserver {
lateinit var getContent : ActivityResultLauncher<String>

override fun onCreate(owner: LifecycleOwner) {
getContent = registry.register("key", owner, ActivityResultContracts.GetContent()) { uri ->
// Handle the returned Uri
}
}

fun selectImage() {
getContent.launch("image/*")
}
}

很简单,将代码移到一个LifecycleObserver的实现类中即可,然后在Activity/Fragment中将这个LifecycleObserver的实现类的观察者对象添加到其本身的lifecycle中即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyFragment : Fragment() {
lateinit var observer : MyLifecycleObserver

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ...
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()
}
}
}

使用总结:

  • 整体来说,终于支持callback回调方式回传结果了,
  • 而且启动时也不用带requestCode了,
  • 并且还支持在Activity/Fragment以外的类中使用。

源码追踪

看一下ComponentActivity中registerForActivityResult 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// androidx.activity.ComponentActivity.java 1.6.1
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return registry.register(
"activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}

@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return registerForActivityResult(contract, mActivityResultRegistry, callback);
}

这里2个重载方法,下面的方法其实是调用了上面的方法,所以本质就是上面的三个参数的方法,也就是 registry.register() 方法,而在Fragment中也有两个类似的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
   // androidx.fragment.app.Fragment.java 1.5.4
@MainThread
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
@Override
public ActivityResultRegistry apply(Void input) {
if (mHost instanceof ActivityResultRegistryOwner) {
return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();
}
return requireActivity().getActivityResultRegistry();
}
}, callback);
}

@MainThread
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
@Override
public ActivityResultRegistry apply(Void input) {
return registry;
}
}, callback);
}

如果继续跟踪prepareCallInternal方法会发现,Fragment中最终还是依托mHost宿主的ActivityResultRegistry对象来执行registry.register() 方法的。

也就是说其实在androidx以后,任意Activity对象中调用getActivityResultRegistry() 方法,它都会返回一个ActivityResultRegistry 对象,然后调用该对象的register方法来注册回调接口,而在Fragment中自然能获取到宿主Activity对象也能拿到这个ActivityResultRegistry 对象。

着重看一下registry.register() 方法:

1
2
3
4
5
6
7
 public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
......
}

可以看出register方法它需要四个参数:

  • key: 唯一标识字符串,代替以前的requestCode,其实内部还是使用的整型的requestCode,只不过是根据传入的key生成随机数然后存入一个Map中,用的时候再从Map中获取。
  • lifecycleOwnerlifecycle持有者,我们知道在androidx以后ActivityFragment默认就是LifecycleOwner接口的实现者
  • contractActivityResultContract<I, O> 类型,契约类,它有两个泛型,一个输入类型,一个输出类型
  • callback:回调接口,代替以前的onActivityResult方法,内部会将前面的key参数和callback保存到一个Map中,以便后面需要的时候方便获取

register方法的返回值是一个 ActivityResultLauncher<I>类型的对象,它就是最终真正用来执行启动动作的对象,通过执行它的launch方法进行启动。

register方法还有一个重载方法:

1
2
3
4
5
6
 public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
......
}

这个方法与上面那个方法的区别就是少了一个lifecycleOwner参数,但是少了这个参数也就意味着你需要自己的onDestroy方法中执行对应的 ActivityResultLauncher.unregister() 方法,不如上面那个方便了。因为带lifecycleOwner参数的register方法内部其实处理好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {

Lifecycle lifecycle = lifecycleOwner.getLifecycle();

if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
+ "attempting to register while current state is "
+ lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
+ "they are STARTED.");
}

registerKey(key); // 注册
...
LifecycleEventObserver observer = new LifecycleEventObserver() {
@Override
@SuppressWarnings("deprecation")
public void onStateChanged(
@NonNull LifecycleOwner lifecycleOwner,
@NonNull Lifecycle.Event event) {
if (Lifecycle.Event.ON_START.equals(event)) {
// 将用户传入的callback封装保存到一个Map中
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
if (mParsedPendingResults.containsKey(key)) {
@SuppressWarnings("unchecked")
final O parsedPendingResult = (O) mParsedPendingResults.get(key);
mParsedPendingResults.remove(key);
callback.onActivityResult(parsedPendingResult);
}
final ActivityResult pendingResult = mPendingResults.getParcelable(key);
if (pendingResult != null) {
mPendingResults.remove(key);
callback.onActivityResult(contract.parseResult(
pendingResult.getResultCode(),
pendingResult.getData()));
}
} else if (Lifecycle.Event.ON_STOP.equals(event)) {
mKeyToCallback.remove(key);
} else if (Lifecycle.Event.ON_DESTROY.equals(event)) {
unregister(key); // 反注册
}
}
};

这里借助生命周期感知那一套,确保了register方法在调用的时候必须在STARTED之前注册,而在ON_DESTROY状态之后则会自动帮你反注册。这里的mKeyToCallback是一个Map<String, CallbackAndContract<?>> 类型的对象,以<key, 用户传入的Callback>形式保存回调。

看一下unregister(key)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final void unregister(@NonNull String key) {
if (!mLaunchedKeys.contains(key)) {
// Only remove the key -> requestCode mapping if there isn't a launch in flight
Integer rc = mKeyToRc.remove(key);
if (rc != null) {
mRcToKey.remove(rc);
}
}
mKeyToCallback.remove(key); // 从map中移除callback对象
......
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer != null) {
lifecycleContainer.clearObservers();
mKeyToLifecycleContainers.remove(key);
}
}

这里一个关键的地方就是LifecycleEventObserver在感知页面销毁时,就在unregister(key)方法中从mKeyToCallback这个map中删除用户传入的callback回调对象,从而避免了Activity销毁之后匿名内部类还会持有Activity对象的引用导致内存泄漏的问题

对于contract参数,在ActivityResultContracts中提供了常用的内置类型,看名字也可以猜出是干嘛的,比如拍视频、选图片等,如果有需要,开发者可以之前从其中选择,如果是普通的Activity之间的跳转就选择StartActivityForResult这个类型即可。

在这里插入图片描述
不过这些类型你也可以统统不用,自己创建也可以,只需实现ActivityResultContract类满足它的输入输出类型约束即可。

对于ActivityResultLauncher.launch方法也没有什么神秘的,内部调用了ActivityResultRegistry 的onLaunch方法,而ActivityResultRegistry 是一个抽象类,其实例对象是在ComponentActivity的构造方法(源码一直在变,今天看又直接放到了成员变量中)创建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public abstract class ActivityResultRegistry {
...
@MainThread
public abstract <I, O> void onLaunch(
int requestCode,
@NonNull ActivityResultContract<I, O> contract,
@SuppressLint("UnknownNullness") I input,
@Nullable ActivityOptionsCompat options);
....
@NonNull
public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
.......
return new ActivityResultLauncher<I>() {
@Override
public void launch(I input, @Nullable ActivityOptionsCompat options) {
Integer innerCode = mKeyToRc.get(key);
if (innerCode == null) {
throw new IllegalStateException("Attempting to launch an unregistered "
+ "ActivityResultLauncher with contract " + contract + " and input "
+ input + ". You must ensure the ActivityResultLauncher is registered "
+ "before calling launch().");
}
mLaunchedKeys.add(key);
try {
onLaunch(innerCode, contract, input, options);
} catch (Exception e) {
mLaunchedKeys.remove(key);
throw e;
}
}
@Override
public void unregister() {
ActivityResultRegistry.this.unregister(key);
}
@NonNull
@Override
public ActivityResultContract<I, ?> getContract() {
return contract;
}
};
}
}
1
2
3
4
5
6
7
8
9
10
public ComponentActivity() {
this.mActivityResultRegistry = new ActivityResultRegistry() {
public <I, O> void onLaunch(final int requestCode, @NonNull ActivityResultContract<I, O> contract, I input, @Nullable ActivityOptionsCompat options) {
ComponentActivity activity = ComponentActivity.this;
...
ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle);
}
}
};
}

这里最终还是执行的Activity的startActivityForResult方法,而在onActivityResult方法中又将结果转发回mActivityResultRegistry对象中:

1
2
3
4
5
6
7
8
9
10
public class ComponentActivity {
...
@Deprecated
@CallSuper
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (!this.mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class ActivityResultRegistry {
...
@MainThread
public final boolean dispatchResult(int requestCode, int resultCode, @Nullable Intent data) {
String key = mRcToKey.get(requestCode);
if (key == null) {
return false;
}
doDispatch(key, resultCode, data, mKeyToCallback.get(key));
return true;
}

private <O> void doDispatch(String key, int resultCode, @Nullable Intent data,
@Nullable CallbackAndContract<O> callbackAndContract) {
if (callbackAndContract != null && callbackAndContract.mCallback != null
&& mLaunchedKeys.contains(key)) {
ActivityResultCallback<O> callback = callbackAndContract.mCallback;
ActivityResultContract<?, O> contract = callbackAndContract.mContract;
callback.onActivityResult(contract.parseResult(resultCode, data));
mLaunchedKeys.remove(key);
} else {
// Remove any parsed pending result
mParsedPendingResults.remove(key);
// And add these pending results in their place
mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
}
}
}

很明显了,这里根据resultCode得到的key就是当初我们在register方法中传入的key,而传入的key又和传入的callback建立了Map映射关系,因此这里可以拿到我们在register方法中传入的callback进行结果回调。

总而言之一句话,只要官方想玩,源码在手,随时可以改,加个回调也是分分钟的事,就是这件事做的有点晚了,在androidx以前开发者对于startActivityForResult操作还是十分痛苦的(明明一整套的姿势,你非得给我拆开)。但是迟来总比没有好,相信很多公司目前仍有很多老项目有大量的代码甚至迁移到androidx都比较困难。

Jetpack Compose中如何正确的startActivityForResult

简单了解了androidx中如何正确的startActivityForResult后,在Jetpack Compose中使用就非常简单了,在Composable中使用主要通过一个rememberLauncherForActivityResult函数,可以看一下其实现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Composable
public fun <I, O> rememberLauncherForActivityResult(
contract: ActivityResultContract<I, O>,
onResult: (O) -> Unit
): ManagedActivityResultLauncher<I, O> {
// Keep track of the current contract and onResult listener
val currentContract = rememberUpdatedState(contract)
val currentOnResult = rememberUpdatedState(onResult)

// It doesn't really matter what the key is, just that it is unique
// and consistent across configuration changes
val key = rememberSaveable { UUID.randomUUID().toString() }

val activityResultRegistry = checkNotNull(LocalActivityResultRegistryOwner.current) {
"No ActivityResultRegistryOwner was provided via LocalActivityResultRegistryOwner"
}.activityResultRegistry
val realLauncher = remember { ActivityResultLauncherHolder<I>() }
val returnedLauncher = remember {
ManagedActivityResultLauncher(realLauncher, currentContract)
}

// DisposableEffect ensures that we only register once
// and that we unregister when the composable is disposed
DisposableEffect(activityResultRegistry, key, contract) {
realLauncher.launcher = activityResultRegistry.register(key, contract) {
currentOnResult.value(it)
}
onDispose {
realLauncher.unregister()
}
}
return returnedLauncher
}

可以看到,它就是在DisposableEffect这个副作用Api中调用了以前的activityResultRegistry.register而已,返回的是一个ManagedActivityResultLauncher<I, O>类型的对象,这个对象中包装了以前的realLaunchercontract对象。所以说使用跟之前其实是差不多类似的。

选择文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun PickImage() {
var uri by remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
uri = it
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { launcher.launch("image/*") }) {
Text(text = "Pick a picture")
}
if (uri != null) {
Image(
painter = rememberAsyncImagePainter(uri) , // 使用coil加载图片
modifier = Modifier.fillMaxWidth(),
contentDescription = null
)
}
}
}

拍照示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Composable
fun TakePhotoForResult() {
var resultBitmap by remember { mutableStateOf<Bitmap?>(null) }
val scope = rememberCoroutineScope()

val outPictureFile = LocalContext.current.getCacheImgFile()
val pictureUri = LocalContext.current.getOutUri(outPictureFile)
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
if (isSuccess) {
scope.launch { resultBitmap = BitmapFactory.decodeFile(outPictureFile.path) }
}
}

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { launcher.launch(pictureUri) }) {
Text(text = "Take a picture")
}
resultBitmap?.let { image ->
Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
}
}
}

private fun Context.getCacheImgFile() : File {
return File(externalCacheDir, "/${System.currentTimeMillis()}.jpeg")
}

private fun Context.getOutUri(file: File) : Uri {
return FileProvider.getUriForFile(this, "${packageName}.provider", file)
}

如果只是想获得一张拍照预览图,可以使用下面的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Composable
fun TakePhotoForResult2() {
var resultBitmap by remember { mutableStateOf<Bitmap?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) { bitmap ->
resultBitmap = bitmap
}

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { launcher.launch() }) {
Text(text = "Take a picture")
}
resultBitmap?.let { image ->
Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
}
}
}

这种方式更加简单,但是这种方式得到的是一个略缩图的尺寸大小,比较模糊。

启动其他的业务Activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun StartActivityForResultExample() {
var result by remember { mutableStateOf("") }
val target = Intent(LocalContext.current, OtherActivity::class.java).apply {
putExtra("name", "张三")
putExtra("uid", 123)
}
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
activityResult.data?.apply {
result = getStringExtra("name").toString()
}
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { launcher.launch(target) }) {
Text(text = "StartActivityForResult")
}
Text(text = "result: $result", fontSize = 22.sp)
}
}

可见使用方式跟之前几乎是一致的。

注:本文示例代码所有涉及到使用权限的地方需要自己提前申请好。


参考:
获取 activity 的结果