Jetpack Compose中的startActivityForResult的正确姿势

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中的startActivityForResult的正确姿势相关的知识,希望对你有一定的参考价值。

之前在 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 : 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中获取。
  • 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 
	...
	@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) 
       

以上是关于Jetpack Compose中的startActivityForResult的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章

JetPack Compose 基础(3)Compose 中的主题

Jetpack Compose 中的作用域状态

Jetpack Compose中的手势操作

Jetpack Compose中的Accompanist

Jetpack Compose中的Canvas

没有从 jetpack compose 中的 rememberLauncherForActivityResult() 获取结果