之前在 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 : 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
方法的实现:
// 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
中也有两个类似的方法:
// 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()
方法:
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 {
...
@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;
}
};
}
}
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
@CallSuper
protected 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 {
...
@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
函数,可以看一下其实现源码:
@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>
类型的对象,这个对象中包装了以前的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 的结果