Why won't my UI update while a background task is running? - kotlin

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?

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.

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() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count.value += 1
}
}
Text(count.value.toString())
}
Remember can be done with delegation to remove the need of using the .value such as:
#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

Should I use rememberCoroutineScope or viewModelScope? Which one is right for my scenario?

In my example, I need to perform a process and if it is successful, the app should navigate to the next page.
The first approach uses viewmodel scope on a non suspend function and then uses a callback to let the UI know that the process is complete. The second approach is declaring a coroutine scope in the UI level and then making the viewmodel a suspend function.
Which would be the right approach for my scenario?
What's the advantage of each approach over the other?
Is there a particular scenario where one approach is more applicable than the other?
First approach:
UI:
Button(
onClick = {
vm.process(
onSuccess = { navcontroller.Navigate("nextpage") },
onFail = { errorMessage.value = it }
)
}
)
ViewModel:
fun process() {
viewmodelScope.launch {
val err = process()
if(err.isBlank()) {
onSuccess()
} else {
onFail(err)
}
}
}
Second approach?
UI:
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
val err = vm.process()
if(err.isBlank()) {
navController.navigate("nextPage")
} else {
errorMessage.value = err
}
}
})
ViewModel:
suspend fun process() : String {
val err = process()
if(err.isBlank()) {
onSuccess()
} else {
onFail(err)
}
}
Qs: What's the difference between rememberCoroutineScope() & viewmodelscope?
The rememberCoroutineScope() is tied to the lifecycle of the composable & the ViewModelScope is the same as the lifecycle scope, the only difference is that the coroutine in this scope will live as long as the view model is alive.
i.e
On orientation change, the coroutines in ViewModelScope will live but the lifecycle scope's coroutines will die/end.

Suspend a call until a value is set

I want class UploadWorker to retrieve a value from class Manager, but that value may not yet be ready in Manager. So I want class UploadWorker to wait until that value is set.
class UploadWorker(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
override fun doWork(): Result {
Manager.isReady()
return Result.success()
}
}
object Manager {
private lateinit var isReady
fun initialize(context: Context, myData: MyData) {
...
isReady = true
}
suspend fun isReady() {
if(::isReady.isInitialized()
return isReady
else // wait here until initialized
}
}
In the else if I could somehow suspend or wait until my MyApplication class calls initialize(). Any ideas?
CompletableDeferred is quite handy for situations like this.
Your code would probably look something like this:
object Manager {
private val initialized = CompletableDeferred<Unit>()
fun initialize(context: Context, myData: MyData) {
...
initialized.complete(Unit)
}
suspend fun awaitInitialized() {
initialized.await()
// initialization is done at this point
}
}
You can use StateFlow to deliver status of initialization:
val isReady = MutableStateFlow(false)
// to wait:
if (isReady.value) return true
else isReady.first { it } // it == true
// to notify:
isReady.value = true
Although StateFlows are quite lightweight, but if you want to do it more lighter but in a ugly way (using coroutine internals directly):
val conts = mutableListOf<Continuation<Boolean>>()
private lateinit var isReady = false
set(value) {
if (value) conts.forEach { it.resume(true) }.also { conts.clear() }
field = value
}
// to wait:
if (isReady) return true
else suspendCancellableCoroutine { cont ->
conts.add(cont)
invokeOnCancellation { conts.remove(cont) }
}
// to notify:
isReady = true

How to subscribe to StateFlow in kotlin-react useEffect

I'm trying to create a small counter example for kotlin-react with functionalComponent with kotlin 1.4-M2.
The example should use kotlinx.coroutines.flow. I'm struggling at collecting the values from the store in reacts useEffect hook.
Store:
object CounterModel { // Modified sample from kotlin StateFlow doc
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter: StateFlow<Int> get() = _counter // publicly exposed as read-only state flow
fun inc() { _counter.value++ }
}
Component:
val counter = functionalComponent<RProps> {
val (counterState, setCounter) = useState(CounterModel.counter.value)
useEffect(listOf()) {
// This does not work
GlobalScope.launch { CounterModel.counter.collect { setCounter(it) } }
}
div {
h1 {
+"Counter: $counterState"
}
button {
attrs.onClickFunction = { CounterModel.inc() }
}
}
}
When I directly call CounterModel.counter.collect { setCounter(it) } it complains about Suspend function 'collect' should be called only from a coroutine or another suspend function.
How would you implement this useEffect hook?
And once the subscription works, how would you unsubscribe from it (use useEffectWithCleanup instead of useEffect)?
Finally found a solution. We can use onEach to do an action for every new value and then 'subscribe' with launchIn. This returns a job that can be canceled for cleanup:
object CounterStore {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> get() = _counter
fun inc() { _counter.value++ }
}
val welcome = functionalComponent<RProps> {
val (counter, setCounter) = useState(CounterStore.counter.value)
useEffectWithCleanup(listOf()) {
val job = CounterStore.counter.onEach { setCounter(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
div {
+"Counter: $counter"
}
button {
attrs.onClickFunction = { CounterStore.inc() }
+"Increment"
}
}
We can extract this StateFlow logic to a custom react hook:
fun <T> useStateFlow(flow: StateFlow<T>): T {
val (state, setState) = useState(flow.value)
useEffectWithCleanup(listOf()) {
val job = flow.onEach { setState(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
return state
}
And use it like this in our component:
val counter = useStateFlow(CounterStore.counter)
The complete project can be found here.
The Flow-Api is very experimental so this might not be the final solution :)
if's very important to check that the value hasn't changed,
before calling setState, otherwise the rendering happens twice
external interface ViewModelProps : RProps {
var viewModel : MyViewModel
}
val App = functionalComponent<ViewModelProps> { props ->
val model = props.viewModel
val (state, setState) = useState(model.stateFlow.value)
useEffectWithCleanup {
val job = model.stateFlow.onEach {
if (it != state) {
setState(it)
}
}.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
}

Async jobs cancellation causes parent cancellation

I tried to run two async jobs.
There is a button when clicked, would cancel one of the jobs. But I noticed when I do it, the other job will be cancelled too.
What happened?
class SplashFragment : BaseFragment(R.layout.fragment_splash), CoroutineScope by MainScope() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launch {
val countdown = async { countDown() }
val ipAndMaintain = async { checkIPAndMaintain() }
btnSkip.onClick {
countdown.cancel() // cancel countdown
btnSkip.isVisible = false
if (!ipAndMaintain.isCompleted) {
showLoadingDialog()
}
}
countdown.await()
startOtherPage(ipAndMaintain.await())
}
}
private suspend fun countDown() {
var time = 3
while (time >= 0) {
btnSkip.text = getString(R.string.skip, time)
delay(1000)
yield()
time--
}
}
private suspend fun checkIPAndMaintain(): Int {
delay(2000)
return 1
}
}
When you call await on a cancelled Deferred it throws an Exception. If you don't catch it then it will be propagated to the parent coroutine which will cancel all its children. Wrap your countdown.await() instruction with a try-catch block and you'll see that the other coroutine continues. That's the effect of structured concurrency.
You can read this article by Roman Elizarov about the topic.

Fan-out / fan-in - closing result channel

I'm producing items, consuming from multiple co-routines and pushing back to resultChannel. Producer is closing its channel after last item.
The code never finishes as resultChannel is never being closed. How to detect and properly finish iteration so hasNext() return false?
val inputData = (0..99).map { "Input$it" }
val threads = 10
val bundleProducer = produce<String>(CommonPool, threads) {
inputData.forEach { item ->
send(item)
println("Producing: $item")
}
println("Producing finished")
close()
}
val resultChannel = Channel<String>(threads)
repeat(threads) {
launch(CommonPool) {
bundleProducer.consumeEach {
println("CONSUMING $it")
resultChannel.send("Result ($it)")
}
}
}
val iterator = object : Iterator<String> {
val iterator = resultChannel.iterator()
override fun hasNext() = runBlocking { iterator.hasNext() }
override fun next() = runBlocking { iterator.next() }
}.asSequence()
println("Starting interation...")
val result = iterator.toList()
println("finish: ${result.size}")
You can run a coroutine that awaits for the consumers to finish and then closes the resultChannel.
First, rewrite the code that starts the consumers to save the Jobs:
val jobs = (1..threads).map {
launch(CommonPool) {
bundleProducer.consumeEach {
println("CONSUMING $it")
resultChannel.send("Result ($it)")
}
}
}
And then run another coroutine that closes the channel once all the Jobs are done:
launch(CommonPool) {
jobs.forEach { it.join() }
resultChannel.close()
}