androidkotlinandroid-jetpack-composekotlin-coroutinesside-effects

Android Jetpack Compose: How to stop execution of a coroutine launched in `produceState`?


Goal

I have a computation heavy function calculateItems: suspend (String) -> Sequence<String>.

I want to load the outputs of this calculation while showing the progress to the user and keeping the UI responsive. I also want to cancel this calculation in case the user input changes, and start again once the user clicks a button.

Approach

I use produceState. Inside produceState, I delegate this computation to a non-Main-dispatcher in order to keep the UI responsive. I also collect the items emitted from that sequence and update the progress on each received item.

val totalSize = 255 // for sake of this example
var input by rememberSaveable(stateSaver = TextFieldValue.Saver) {
    mutableStateOf(TextFieldValue("Test Input"))
}
var doCalculation by rememberSaveable(input) {
    mutableStateOf(false)
}
val resultState by produceState(
    State.Null as State,
    input,
    doCalculation,
) {
    value = DealingState.Null
    if (!doCalculation) {
        value = DealingState.Null
        return@produceState
    }

    value = DealingState.Loading(
        progress = 0,
        required = totalSize,
    )

    launch(Dispatchers.Default) {
        runCatching {
            val resultList = mutableListOf<String>()
            calculateItems(input).forEach {
                resultList.add(it)
                value = State.Loading(
                    progress = resultList.size,
                    required = totalSize,
                )
            }
            value = State.Success(resultList)
        }.getOrElse {
            value = State.Failure(it)
        }
    }
}

What I Tried

I tried the following things:

val scope = CoroutineScope(Dispatchers.Default)
val resultState by produceState(...) {
    scope.cancel()
    ....
    scope.launch(Dispatchers.Default) {...}
}
var job: Job? = null
val resultState by produceState(...) {
    job?.cancel() // and job?.cancelChildren()
    ....
    job = launch(Dispatchers.Default) {...}
}

Solution

  • Try making your inner coroutine cancellable by adding ensureActive(), for example:

    calculateItems(input).forEach {
        ensureActive()
        resultList.add(it)