Home > front end >  Why won't my UI update while a background task is running?
Why won't my UI update while a background task is running?

Time:10-25

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