【问题标题】:Jetpack Compose: Launch ActivityResultContract request from Composable functionJetpack Compose:从 Composable 函数启动 ActivityResultContract 请求
【发布时间】:2021-02-19 13:50:20
【问题描述】:

1.2.0-beta01androidx.activity:activity-ktx 开始,不能再launch 使用Activity.registerForActivityResult() 创建的请求,如上面“行为更改”下的链接中突出显示的那样,并在Google issue here 中看到。

应用程序现在应该如何通过@Composable 函数启动这个请求?以前,应用程序可以通过使用AmbientMainActivity 的实例沿链向下传递,然后轻松启动请求。

可以通过以下方式解决新行为,例如,在 Activity 的 onCreate 函数之外实例化后,将注册活动结果的类沿链向下传递,然后在 Composable 中启动请求。但是,无法通过这种方式注册完成后执行的回调。

可以通过创建自定义ActivityResultContract 来解决此问题,该ActivityResultContract 在启动时会进行回调。但是,这意味着几乎所有内置的 ActivityResultContracts 都不能与 Jetpack Compose 一起使用。

TL;DR

应用如何从@Composable 函数启动ActivityResultsContract 请求?

【问题讨论】:

    标签: android android-jetpack android-jetpack-compose registerforactivityresult


    【解决方案1】:

    androidx.activity:activity-compose:1.3.0-alpha06 起,registerForActivityResult() API 已重命名为 rememberLauncherForActivityResult(),以更好地表明返回的 ActivityResultLauncher 是代您记住的托管对象。

    val result = remember { mutableStateOf<Bitmap?>(null) }
    val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
        result.value = it
    }
    
    Button(onClick = { launcher.launch() }) {
        Text(text = "Take a picture")
    }
    
    result.value?.let { image ->
        Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
    }
    

    【讨论】:

    • 这现在应该是公认的答案,因为 rememberLauncherForActivityResult 现在是正确的方法。
    • @JamesBlack 我做到了。容易多了!
    【解决方案2】:

    Activity Result 有两个 API 界面:

    • 核心ActivityResultRegistry。这就是底层工作的实际工作。
    • ActivityResultCaller 中的便利接口,ComponentActivityFragment 实现了将 Activity Result 请求与 Activity 或 Fragment 的生命周期联系起来

    Composable 与 Activity 或 Fragment 具有不同的生命周期(例如,如果您从层次结构中删除 Composable,它应该自行清理),因此使用 ActivityResultCaller API,例如 registerForActivityResult() 绝不是正确的要做的事情。

    相反,您应该直接使用ActivityResultRegistry API,直接调用register()unregister()。这最好与 rememberUpdatedState()DisposableEffect 搭配使用,以创建可与 Composable 一起使用的 registerForActivityResult 版本:

    @Composable
    fun <I, O> registerForActivityResult(
        contract: ActivityResultContract<I, O>,
        onResult: (O) -> Unit
    ) : ActivityResultLauncher<I> {
        // First, find the ActivityResultRegistry by casting the Context
        // (which is actually a ComponentActivity) to ActivityResultRegistryOwner
        val owner = ContextAmbient.current as ActivityResultRegistryOwner
        val activityResultRegistry = owner.activityResultRegistry
    
        // Keep track of the current onResult listener
        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 = rememberSavedInstanceState { UUID.randomUUID().toString() }
    
        // Since we don't have a reference to the real ActivityResultLauncher
        // until we register(), we build a layer of indirection so we can
        // immediately return an ActivityResultLauncher
        // (this is the same approach that Fragment.registerForActivityResult uses)
        val realLauncher = mutableStateOf<ActivityResultLauncher<I>?>(null)
        val returnedLauncher = remember {
            object : ActivityResultLauncher<I>() {
                override fun launch(input: I, options: ActivityOptionsCompat?) {
                    realLauncher.value?.launch(input, options)
                }
    
                override fun unregister() {
                    realLauncher.value?.unregister()
                }
    
                override fun getContract() = contract
            }
        }
    
        // DisposableEffect ensures that we only register once
        // and that we unregister when the composable is disposed
        DisposableEffect(activityResultRegistry, key, contract) {
            realLauncher.value = activityResultRegistry.register(key, contract) {
                currentOnResult.value(it)
            }
            onDispose {
                realLauncher.value?.unregister()
            }
        }
        return returnedLauncher
    }
    
    

    然后可以通过以下代码在您自己的 Composable 中使用它:

    val result = remember { mutableStateOf<Bitmap?>(null) }
    val launcher = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) {
        // Here we just update the state, but you could imagine
        // pre-processing the result, or updating a MutableSharedFlow that
        // your composable collects
        result.value = it
    }
    
    // Now your onClick listener can call launch()
    Button(onClick = { launcher.launch() } ) {
        Text(text = "Take a picture")
    }
    
    // And you can use the result once it becomes available
    result.value?.let { image ->
        Image(image.asImageAsset(),
            modifier = Modifier.fillMaxWidth())
    }
    

    【讨论】:

    • 您是否有任何计划将注册表公开为环境,例如ActivityResultRegistryAmbient?将ContextAmbient 投放到活动中是否存在不当行为?
    • 您可以为 feature request issue 加注星标,以使这部分成为 Compose 本身。 IMO,@ 987654345@ 不是很有帮助,因为您永远不想在registerForActivityResult() 之类的托管范围之外使用它。请注意,您不需要任何活动 - 只是通用的 ActivityResultRegistryOwner,但出于实际目的,setContent 要求您无论如何都在 ComponentActivity 内,因此此转换始终成功。
    • @ianhanniballake 直接从马的嘴里!这很好用。谢谢你的例子!
    • @ianhanniballake 我不知道为什么,但这种解决方案被证明是极其不可靠和不可预测的。 currentOnResult.value(it) 似乎只是调用sometimes,我不知道为什么。它非常令人沮丧。
    • @Jeyhey - 确保您使用的是 Activity 1.2.0-beta02(以及使用它的 Fragment 1.3.0-beta02 来获取FragmentActivity / AppCompatActivity 中的相关修复)。听起来你想给上面 cmets 中提到的 the feature request 加注星标,以使其成为“系统实用功能”。
    【解决方案3】:

    Activity Compose 1.3.0-alpha03 及以后开始,有一个新的实用函数 registerForActivityResult() 可以简化此过程。

    @Composable
    fun RegisterForActivityResult() {
        val result = remember { mutableStateOf<Bitmap?>(null) }
        val launcher = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) {
            result.value = it
        }
    
        Button(onClick = { launcher.launch() }) {
            Text(text = "Take a picture")
        }
    
        result.value?.let { image ->
            Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
        }
    }
    

    (来自here给出的样本)

    【讨论】:

      【解决方案4】:

      对于那些没有通过@ianhanniballake 提供的要点获得结果的人,在我的例子中,returnedLauncher 实际上捕获了realLauncher 的已处置值。

      因此,虽然删除间接层应该可以解决问题,但它绝对不是这样做的最佳方式。

      这是更新版本,直到找到更好的解决方案:

      @Composable
      fun <I, O> registerForActivityResult(
          contract: ActivityResultContract<I, O>,
          onResult: (O) -> Unit
      ): ActivityResultLauncher<I> {
          // First, find the ActivityResultRegistry by casting the Context
          // (which is actually a ComponentActivity) to ActivityResultRegistryOwner
          val owner = AmbientContext.current as ActivityResultRegistryOwner
          val activityResultRegistry = owner.activityResultRegistry
      
          // Keep track of the current onResult listener
          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 = rememberSavedInstanceState { UUID.randomUUID().toString() }
      
          // TODO a working layer of indirection would be great
          val realLauncher = remember<ActivityResultLauncher<I>> {
              activityResultRegistry.register(key, contract) {
                  currentOnResult.value(it)
              }
          }
      
          onDispose {
              realLauncher.unregister()
          }
          
          return realLauncher
      }
      

      【讨论】:

        【解决方案5】:

        添加以防有人开始新的外部意图。就我而言,我想在点击 jetpack compose 中的按钮时启动 google 登录提示。

        声明你的意图启动

        val startForResult =
            rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
                if (result.resultCode == Activity.RESULT_OK) {
                    val intent = result.data
                    //do something here
                }
            }
        

        启动您的新活动或任何意图。

         Button(
                onClick = {
                    //important step
                    startForResult.launch(googleSignInClient?.signInIntent)
                },
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(start = 16.dp, end = 16.dp),
                shape = RoundedCornerShape(6.dp),
                colors = ButtonDefaults.buttonColors(
                    backgroundColor = Color.Black,
                    contentColor = Color.White
                )
            ) {
                Image(
                    painter = painterResource(id = R.drawable.ic_logo_google),
                    contentDescription = ""
                )
                Text(text = "Sign in with Google", modifier = Modifier.padding(6.dp))
            }
        

        #googlesignin

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-11-11
          • 2022-09-25
          • 2023-01-05
          • 2023-01-24
          • 1970-01-01
          相关资源
          最近更新 更多