之前在 Jetpack Compose中的导航路由 里简单的提到了从 Compose
导航到其他 Activity
页面的方式,对于不带返回结果的则就是跟以前一样简单的启动Activity
的代码,而如果是startActivityForResult
方式的,需要使用带回调的方式去启动,那么在以前,我们要么是使用三方库,要么是自己封装一个简单的库来使用 (至于背后原理也不是什么新鲜事了) 。
后来研究了一下,在Compose
中startActivityForResult
也有了新的姿势,要理解Compose
中startActivityForResult
的姿势,这还得从androidx
的startActivityForResult
的姿势说起,因为Compose
就是在androidx
的基础上利用其API简单封装了一下而已。
倒也不是说以前的不能用了,毕竟有系统自带的,谁还去用第三方的呀,用第三方的还得导入一个依赖库不是,能少依赖三方的就尽量少依赖吧。
androidx之后如何正确的startActivityForResult
如何使用
如果是在Activity
或Fragment
内部使用的话,直接调用registerForActivityResult
方法即可。
例如,选择文件:
val launcher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->uri?.apply { showToast(uri.toString()) }}launcher.launch("image/*")
ActivityResultContracts.GetContent()
的方式获取文件launch
时需要通过指定 mime type
来过滤文件类型, 例如 image/*
,这会打开系统自带的一个文件选择器供你选择文件。
录制视频:
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)
拍照:
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之间的跳转:
// 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)
// 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以外的类中使用:
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
中即可。
class MyFragment : Fragment() {lateinit var observer : MyLifecycleObserveroverride 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 imageobserver.selectImage()}}
}
使用总结:
- 整体来说,终于支持
callback
回调方式回传结果了, - 而且启动时也不用带
requestCode
了, - 并且还支持在
Activity/Fragment
以外的类中使用。
源码追踪
看一下ComponentActivity中registerForActivityResult
方法的实现:
// androidx.activity.ComponentActivity.java 1.6.1@NonNull@Overridepublic 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@Overridepublic final <I, O> ActivityResultLauncher<I> registerForActivityResult(@NonNull ActivityResultContract<I, O> contract,@NonNull ActivityResultCallback<O> callback) {return registerForActivityResult(contract, mActivityResultRegistry, callback);}
这里2个重载方法,下面的方法其实是调用了上面的方法,所以本质就是上面的三个参数的方法,也就是 registry.register()
方法,而在Fragment
中也有两个类似的方法:
// androidx.fragment.app.Fragment.java 1.5.4@MainThread@NonNull@Overridepublic final <I, O> ActivityResultLauncher<I> registerForActivityResult(@NonNull final ActivityResultContract<I, O> contract,@NonNull final ActivityResultCallback<O> callback) {return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {@Overridepublic ActivityResultRegistry apply(Void input) {if (mHost instanceof ActivityResultRegistryOwner) {return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();}return requireActivity().getActivityResultRegistry();}}, callback);}@MainThread@NonNull@Overridepublic 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>() {@Overridepublic ActivityResultRegistry apply(Void input) {return registry;}}, callback);}
如果继续跟踪prepareCallInternal
方法会发现,Fragment
中最终还是依托mHost
宿主的ActivityResultRegistry
对象来执行registry.register()
方法的。
也就是说其实在androidx
以后,任意Activity
对象中调用getActivityResultRegistry()
方法,它都会返回一个ActivityResultRegistry
对象,然后调用该对象的register
方法来注册回调接口,而在Fragment
中自然能获取到宿主Activity
对象也能拿到这个ActivityResultRegistry
对象。
着重看一下registry.register()
方法:
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
中获取。lifecycleOwner
:lifecycle
持有者,我们知道在androidx
以后Activity
和Fragment
默认就是LifecycleOwner
接口的实现者contract
:ActivityResultContract<I, O>
类型,契约类,它有两个泛型,一个输入类型,一个输出类型callback
:回调接口,代替以前的onActivityResult
方法,内部会将前面的key
参数和callback
保存到一个Map
中,以便后面需要的时候方便获取
register
方法的返回值是一个 ActivityResultLauncher<I>
类型的对象,它就是最终真正用来执行启动动作的对象,通过执行它的launch
方法进行启动。
register
方法还有一个重载方法:
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
方法内部其实处理好了:
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)) {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
状态之后则会自动帮你反注册。
对于contract
参数,在ActivityResultContracts
中提供了常用的内置类型,看名字也可以猜出是干嘛的,比如拍视频、选图片等,如果有需要,开发者可以之前从其中选择,如果是普通的Activity之间的跳转就选择StartActivityForResult
这个类型即可。
不过这些类型你也可以统统不用,自己创建也可以,只需实现ActivityResultContract
类满足它的输入输出类型约束即可。
对于ActivityResultLauncher.launch
方法也没有什么神秘的,内部调用了ActivityResultRegistry
的onLaunch
方法,而ActivityResultRegistry
是一个抽象类,其实例对象是在ComponentActivity
的构造方法创建的:
public abstract class ActivityResultRegistry {...@MainThreadpublic abstract <I, O> void onLaunch(int requestCode,@NonNull ActivityResultContract<I, O> contract,@SuppressLint("UnknownNullness") I input,@Nullable ActivityOptionsCompat options);....@NonNullpublic 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>() {@Overridepublic 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;}}@Overridepublic void unregister() {ActivityResultRegistry.this.unregister(key);}@NonNull@Overridepublic ActivityResultContract<I, ?> getContract() {return contract;}};}
}
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
对象中:
public class ComponentActivity {...@Deprecated@CallSuperprotected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {if (!this.mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {super.onActivityResult(requestCode, resultCode, data);}}
}
public abstract class ActivityResultRegistry {...@MainThreadpublic 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 resultmParsedPendingResults.remove(key);// And add these pending results in their placemPendingResults.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
函数,可以看一下其实现源码:
@Composable
public fun <I, O> rememberLauncherForActivityResult(contract: ActivityResultContract<I, O>,onResult: (O) -> Unit
): ManagedActivityResultLauncher<I, O> {// Keep track of the current contract and onResult listenerval 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 changesval key = rememberSaveable { UUID.randomUUID().toString() }val activityResultRegistry = checkNotNull(LocalActivityResultRegistryOwner.current) {"No ActivityResultRegistryOwner was provided via LocalActivityResultRegistryOwner"}.activityResultRegistryval 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 disposedDisposableEffect(activityResultRegistry, key, contract) {realLauncher.launcher = activityResultRegistry.register(key, contract) {currentOnResult.value(it)}onDispose {realLauncher.unregister()}}return returnedLauncher
}
可以看到,它就是在DisposableEffect
这个副作用Api中调用了以前的activityResultRegistry.register
而已,返回的是一个ManagedActivityResultLauncher<I, O>
类型的对象,这个对象中包装了以前的realLauncher
和contract
对象。所以说使用跟之前其实是差不多类似的。
选择文件示例:
@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)}}
}
拍照示例:
@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)
}
如果只是想获得一张拍照预览图,可以使用下面的方式:
@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:
@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 的结果