Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call Kotlin coroutine in composable function callbacks?

I want to call a suspend-function inside of a callback of composable-function.

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

}

If I would have used Rx then I could just subscribe.

I could do invokeOnCompletion and then getCompleted, but that API is experimental.

I can't use launchInComposition in getLocationOnClick because launchInComposition is @Composable and getLocationOnClick can not be @Composable.

What would be the best way to get result of a suspending function inside a regular function, inside @Composable function?

like image 563
Nycta Avatar asked Sep 29 '20 09:09

Nycta


People also ask

How do you call a composable function from suspend?

To call suspend functions safely from inside a composable, use the LaunchedEffect composable. When LaunchedEffect enters the Composition, it launches a coroutine with the block of code passed as a parameter. The coroutine will be cancelled if LaunchedEffect leaves the composition.

How do I launch coroutine Kotlin?

launch. The simplest way to create a coroutine is by calling the launch builder on a specified scope. It Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled.

What is composable function in Kotlin?

Composable functions are the fundamental building blocks of an application built with Compose. Composable can be applied to a function or lambda to indicate that the function/lambda can be used as part of a composition to describe a transformation from application data into a tree or hierarchy.

Why launch coroutine is fire and forget?

UNDISPATCHED, which starts execution in the current thread, then suspends at the first suspension point and, on continuation, uses the dispatcher from its context. This form of launching a coroutine immediately returns the control back to the caller. It is usually known as “fire-and-forget”.


3 Answers

Create a coroutines scope, tied to the lifecycle of your composable, and use that scope to call your suspending function

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")
    }
}
like image 66
heyheyhey Avatar answered Oct 16 '22 09:10

heyheyhey


This works for me:

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

I also tried out the following helper function, to force dev-colleagues to handle Exceptions and to finally cleanup state (also to make the same code (maybe!?) a bit shorter and (maybe!?) a bit more readable):

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

and here's how to use that helper method:

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

}

like image 39
Dirk Hoffmann Avatar answered Oct 16 '22 09:10

Dirk Hoffmann


You can use the viewModelScope of a ViewModel or any other coroutine scope.

Example of Deleting Action for an Item from LazyColumnFor which requires a suspend call handled by a 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 }
            )
        }
    }
like image 4
2jan222 Avatar answered Oct 16 '22 11:10

2jan222