如何在可组合函数回调中调用 Kotlin 协程?

Posted

技术标签:

【中文标题】如何在可组合函数回调中调用 Kotlin 协程?【英文标题】:How to call Kotlin coroutine in composable function callbacks? 【发布时间】:2021-01-14 20:58:51 【问题描述】:

我想在可组合函数的回调中调用挂起函数。

suspend fun getLocation(): Location?  /* ... */ 

@Composable
fun F() 

    val (location, setLocation) = remember  mutableStateOf<Location?>(null) 

    val getLocationOnClick: () -> Unit = 
        /* setLocation __MAGIC__ getLocation */
    

    Button(onClick = getLocationOnClick) 
        Text("detectLocation")
    


如果我会使用 Rx,那么我可以只使用 subscribe

我可以先使用invokeOnCompletion,然后再使用getCompleted,但该 API 是实验性的。

我不能在getLocationOnClick 中使用launchInComposition,因为launchInComposition@ComposablegetLocationOnClick 不能是@Composable

@Composable 函数内的常规函数​​内获取挂起函数结果的最佳方法是什么?

【问题讨论】:

您可以调用 ViewModel 的函数,该函数在 viewmodelScope 上启动挂起函数 否则,如果您有对 AppCompatActivity 的引用,则可以使用其生命周期范围 你的意思是像SomeScope.async setLocation(getLocation()) ?谢谢,这确实有效,我没想到它会那么简单。请您将其评论为答案(以便我可以将此问题标记为已解决)。 【参考方案1】:

创建一个与可组合的生命周期相关联的协程范围,并使用该范围来调用您的挂起函数

suspend fun getLocation(): Location?  /* ... */ 

@Composable
fun F() 
    // Returns a scope that's cancelled when F is removed from composition
    val coroutineScope = rememberCoroutineScope()

    val (location, setLocation) = remember  mutableStateOf<Location?>(null) 

    val getLocationOnClick: () -> Unit = 
        coroutineScope.launch 
            val location = getLocation()
        
    

    Button(onClick = getLocationOnClick) 
        Text("detectLocation")
    

【讨论】:

【参考方案2】:

您可以使用 ViewModel 的 viewModelScope 或任何其他协程范围。

从 LazyColumnFor 中删除项目的操作示例,这需要由 ViewModel 处理的挂起调用。

     class ItemsViewModel : ViewModel() 

        private val _itemList = MutableLiveData<List<Any>>()
        val itemList: LiveData<List<Any>>
            get() = _itemList

        fun deleteItem(item: Any) 
            viewModelScope.launch(Dispatchers.IO) 
                TODO("Fill Coroutine Scope with your suspend call")       
            
        
    

    @Composable
    fun Example() 
        val itemsVM: ItemsViewModel = viewModel()
        val list: State<List<Any>?> = itemsVM.itemList.observeAsState()
        list.value.let  it: List<Any>? ->
            if (it != null) 
                LazyColumnFor(items = it)  item: Any ->
                    ListItem(
                        item = item,
                        onDeleteSelf = 
                            itemsVM.deleteItem(item)
                        
                    )
                
             // else EmptyDialog()
        
    

    @Composable
    private fun ListItem(item: Any, onDeleteSelf: () -> Unit) 
        Row 
            Text(item.toString())
            IconButton(
                onClick = onDeleteSelf,
                icon =  Icons.Filled.Delete 
            )
        
    

【讨论】:

在viewModelScope extension source code 上抢先一步。可以在没有 ViewModel 的情况下使用 val scope = remember CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) 然后 onActive onDispose scope.cancel() 吗? 不太好,你真的不知道操作什么时候结束。只需使用 rememberCoroutinesScope() 并将 deleteItem() 变成一个挂起函数【参考方案3】:

这对我有用:

@Composable
fun TheComposable() 

    val coroutineScope = rememberCoroutineScope()
    val (loadResult, setLoadResult) = remember  mutableStateOf<String?>(null) 

    IconButton(
        onClick = 
            someState.startProgress("Draft Loading...")
            coroutineScope.launch 
                withContext(Dispatchers.IO) 
                    try 
                        loadResult = DataAPI.getData() // <-- non-suspend blocking method
                     catch (e: Exception) 
                        // handle exception
                     finally 
                        someState.endProgress()
                    
                
            

        
    ) 
        Icon(Icons.TwoTone.Call, contentDescription = "Load")
    

我还尝试了以下辅助函数,以强制开发同事处理异常并最终清理状态(也使相同的代码(也许!?)更短一点并且(也许!?)更具可读性):

fun launchHelper(coroutineScope: CoroutineScope,
                 catchBlock: (Exception) -> Unit,
                 finallyBlock: () -> Unit,
                 context: CoroutineContext = EmptyCoroutineContext,
                 start: CoroutineStart = CoroutineStart.DEFAULT,
                 block: suspend CoroutineScope.() -> Unit
): Job 
    return coroutineScope.launch(context, start) 
        withContext(Dispatchers.IO) 
            try 
                block()
             catch (e: Exception) 
                catchBlock(e)
             finally 
                finallyBlock()
            
        
    

下面是如何使用该辅助方法:

@Composable
fun TheComposable() 

    val coroutineScope = rememberCoroutineScope()
    val (loadResult, setLoadResult) = remember  mutableStateOf<String?>(null) 

    IconButton(
        onClick = 
            someState.startProgress("Draft Loading...")
            launchHelper(coroutineScope,
                catchBlock =  e -> myExceptionHandling(e) ,
                finallyBlock =  someState.endProgress() 
            ) 
                loadResult = DataAPI.getData() // <-- non-suspend blocking method
            

        
    ) 
        Icon(Icons.TwoTone.Call, contentDescription = "Load")
    



【讨论】:

以上是关于如何在可组合函数回调中调用 Kotlin 协程?的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin回调函数转协程

Kotlin 协程协程启动 ③ ( 协程组合并发 | 挂起函数串行执行 | 协程组合并发执行挂起函数 )

Kotlin 协程协程启动 ③ ( 协程组合并发 | 挂起函数串行执行 | 协程组合并发执行挂起函数 )

如何使用 kotlin 协程处理回调

Kotlin 协程Flow 流组合 ( Flow#zip 组合多个流 | 新组合流的元素收集间隔与被组合流元素发射间隔的联系 )

Kotlin 协程Flow 流组合 ( Flow#zip 组合多个流 | 新组合流的元素收集间隔与被组合流元素发射间隔的联系 )