I have this code that should show a counter while a background task is running:
@Composable fun startIt() {
val scope = rememberCoroutineScope()
val running = remember { mutableStateOf(false) }
Button({ scope.launch { task(running) } }) {
Text("start")
}
if (running.value) Counter()
}
@Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(100.milliseconds)
count.value = 1
}
}
Text(count.toString())
}
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
coroutineScope {
launch {
// some code that blocks this thread
}
}
running.value = false
}
If I understand correctly, the coroutineScope
block in task
should unblock the main thread, so that the LaunchedEffect
in Counter
can run. But that block never gets past the first delay
, and never returns or gets cancelled. The counter keeps showing 0 until the task finishes.
How do I allow Compose to update the UI properly?
CodePudding user response:
coroutineScope
doesn't change the coroutine context, so I think you're launching a child coroutine that runs in the same thread.
The correct way to synchronously do blocking work in a coroutine without blocking the thread is by using withContext(Dispatchers.IO)
:
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
withContext(Dispatchers.IO) {
// some code that blocks this thread
}
running.value = false
}
If the blocking work is primarily CPU-bound, it is more appropriate to use Dispatchers.Default
instead, I think because it helps prevent the backing thread pool from spawning more threads than necessary for CPU work.
CodePudding user response:
This was a small issue of the way count was being modified, and not of coroutines. To fix your code, the remember for count in Counter() needed to be updated to :
@OptIn(ExperimentalTime::class)
@Composable private fun Counter() {
var count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count = 1
}
}
Text(count.toString())
}
Compose does coroutines slightly differently than Kotlin would by default, this is a small example that shows a bit more of how Compose likes Coroutines to be done:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Compose_coroutinesTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
BaseComponent()
}
}
}
}
}
// BaseComponent holds most of the state, child components respond to its values
@Composable
fun BaseComponent() {
var isRunning by remember { mutableStateOf(false) }
val composableScope = rememberCoroutineScope()
val count = remember { mutableStateOf(0) }
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: ${count.value}")
// Using the async context of a button click, we can toggle running off and on, as well as run our background task for incrementing the counter
ToggleCounter(isRunning) {
isRunning = !isRunning
composableScope.launch {
while (isRunning) {
delay(100L)
count.value = 1
}
}
}
}
}
// Accepting an onTap function and passing it into our button, allows us to modify state as a result of the button, without the button needing to know anything more
@Composable
fun ToggleCounter(isRunning: Boolean, onTap: () -> Unit) {
val buttonText = if (isRunning) "Stop" else "Start"
Button(onClick = onTap) {
Text(buttonText)
}
}