Jetpack Compose中的startActivityForResult的正确姿势

news/2024/11/7 22:35:36/

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

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

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

androidx之后如何正确的startActivityForResult

如何使用

如果是在ActivityFragment内部使用的话,直接调用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中获取。
  • lifecycleOwnerlifecycle持有者,我们知道在androidx以后ActivityFragment默认就是LifecycleOwner接口的实现者
  • contractActivityResultContract<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方法也没有什么神秘的,内部调用了ActivityResultRegistryonLaunch方法,而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>类型的对象,这个对象中包装了以前的realLaunchercontract对象。所以说使用跟之前其实是差不多类似的。

选择文件示例:

@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 的结果


http://www.ppmy.cn/news/11453.html

相关文章

C++ 居然超过了 Python?

很难想象&#xff0c;从 1983 年被正式命名开始算起&#xff0c;已经走过整整四十年光阴的 C&#xff0c;居然力挫 C、Python&#xff0c;摘得了 TIOBE 2022 年度编程语言的桂冠。据 TIOBE 官网显示&#xff0c;在过去一年里&#xff0c;C 因为涨幅最大&#xff0c;为 4.62%&am…

Win10专业版系统Docker安装、配置和使用详细教程

一、win10专业版系统首先需要开启硬件虚拟化及Hyper-V功能&#xff0c;才能进行Docker for Windows软件安装。 如何开启硬件虚拟化&#xff0c;自行百度。可在任务栏中查看虚拟化是否开启。 win10系统&#xff0c;打开控制面板-“应用”-“程序和功能”&#xff0c;开启Hyper-V…

华中科技大学计算机组成原理-计算机数据表示实验(全部通关)

计算机数据表示实验(HUST) 计算机数据表示目录 [建议收藏]计算机数据表示实验(HUST)第1关 汉字国标码转区位码实验第2关 汉字机内码获取实验第3关 偶校验编码设计第4关 偶校验解码电路设计第5关 16位海明编码电路设计第6关 16位海明解码电路设计第7关 海明编码流水传输实验第8关…

如何计算结构体的大小?结构体内存对齐【C语言】

今天我们来讲讲结构体的大小如何来计算 其中涉及到一个结构体中的热门考点&#xff1a;结构体内存对齐 话不多说&#xff0c;开始学习&#xff01; 要想计算结构体的大小&#xff0c;首先要了解结构体的对齐规则。 目录 结构体内存对齐规则 举例 为什么存在内存对齐? 如…

Spring Boot 日志详解

Spring Boot 日志一、日志有什么用二、日志怎么用三、自定义日志打印3.1 在程序中得到日志对象3.2 使用日志对象打印日志四、日志级别4.1 日志级别有什么用4.2 日志级别的分类与使用4.3 日志级别设置五、日志持久化5.1 配置文件名5.2 配置保存路径六、更简单的日志输出 -- lomb…

python直接赋值、浅拷贝与深拷贝

本文主要参考这篇博文python浅拷贝与深拷贝 基本概念 首先了解python中的一些基本概念 变量&#xff1a;是一个系统表的元素&#xff0c;拥有指向对象的连接空间对象&#xff1a;被分配的一块内存&#xff0c;存储其所代表的值引用&#xff1a;是自动形成的从变量到对象的指…

118页4万字智慧检务大数据平台解决方案

【版权声明】本资料来源网络&#xff0c;知识分享&#xff0c;仅供个人学习&#xff0c;请勿商用。【侵删致歉】如有侵权请联系小编&#xff0c;将在收到信息后第一时间删除&#xff01;完整资料领取见文末&#xff0c;部分资料内容&#xff1a; 目录 第1章 前言 1.1、 政策背…

JAVA中的for循环使用方法

一. 循环结构1. 概念在学习Java里的循环之前&#xff0c;我们先来了解一下到底什么是循环&#xff0c;以及循环的作用。我们先来看下面这张图&#xff1a;大家想一下&#xff0c;我们在400米的跑道上参加万米长跑&#xff0c;正常情况下要跑25圈&#xff0c;这25圈每一圈的跑步…