¿Cómo llamar a Kotlin coroutine en devoluciones de llamada de funciones componibles?

4 minutos de lectura

Avatar de usuario de Nycta
Nycta

Quiero llamar a una función de suspensión dentro de una devolución de llamada de función componible.

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")
    }

}

Si hubiera usado Rx, entonces podría simplemente subscribe.

Yo podría hacer invokeOnCompletion y luego getCompletedpero esa API es experimental.

no puedo usar launchInComposition en getLocationOnClick porque launchInComposition es @Composable y getLocationOnClick no puede ser @Composable.

¿Cuál sería la mejor manera de obtener el resultado de una función de suspensión dentro de una función normal, dentro @Composable ¿función?

  • Puede llamar a una función de ViewModel que inicia una función de suspensión en viewmodelScope

    – 2ene222

    29 de septiembre de 2020 a las 9:26

  • De lo contrario, si tiene una referencia a AppCompatActivity, podría usar su ámbito de ciclo de vida.

    – 2ene222

    29 de septiembre de 2020 a las 9:27

  • Te refieres a SomeScope.async { setLocation(getLocation()) }? Gracias, eso realmente funciona, no esperaba que fuera tan simple. ¿Podría comentar eso como respuesta (para que pueda marcar esta pregunta como resuelta)?

    – Nycta

    30 de septiembre de 2020 a las 7:27

Cree un alcance de rutinas, vinculado al ciclo de vida de su componible, y use ese alcance para llamar a su función de suspensión

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")
    }
}

Avatar de usuario de Dirk Hoffmann
Dirk Hoffman

Esto funciona para mí:

@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")
    }

También probé la siguiente función de ayuda, para forzar a los colegas desarrolladores a manejar Excepciones y finalmente limpiar el estado (también para hacer el mismo código (¡quizás!?) un poco más corto y (¡quizás!?) un poco más legible):

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()
            }
        }
    }
}

y he aquí cómo usar ese método auxiliar:

@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")
    }

}

Puede usar viewModelScope de un ViewModel o cualquier otro ámbito de rutina.

Ejemplo de acción de eliminación para un elemento de LazyColumnFor que requiere una llamada de suspensión manejada por un modelo de vista.

     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 }
            )
        }
    }

  • echó un vistazo a la código fuente de la extensión viewModelScope. ¿Podría usarse sin ViewModel solo con val scope = remember { CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) } y luego onActive { onDispose { scope.cancel() } }?

    – Nycta

    1 de octubre de 2020 a las 5:33

  • No muy bien, realmente no sabes cuándo termina la operación. Simplemente use RememberCoroutinesScope() y convierta deleteItem() en una función de suspensión

    – Hey hey hey

    14 de noviembre de 2020 a las 16:18

  • Estoy de acuerdo con @heyheyhey, no sabemos cuándo se llamará al componible, tal vez se recuerde varias veces, por lo que es mejor usarlo rememberCoroutineScope encima viewModelScope

    – gtxtremo

    27 de mayo de 2022 a las 4:34

¿Ha sido útil esta solución?