Kotlin Multiplatform Mobile: Ktor – how to cancel active coroutine (network request, background work) in Kotlin Native (iOS)?

In my project I write View and ViewModel natively and share Repository, Db, networking.

When user navigates from one screen to another, I want to cancel all network requests or other heavy background operations that are currently running in the first screen.

Example function in Repository class:

@Throws(Throwable::class)
suspend fun fetchData(): List<String>

In Android’s ViewModel I can use viewModelScope to automatically cancel all active coroutines. But how to cancel those tasks in iOS app?

Answer

I didn’t find any first party information about this or any good solution, so I came up with my own. Shortly, it will require turning repository suspend functions to regular functions with return type of custom interface that has cancel() member function. Function will take action lambda as parameter. On implementation side, coroutine will be launched and reference for Job will be kept so later when it is required to stop background work interface cancel() function will cancel job.

In addition, because it is very hard to read type of error (in case it happens) from NSError, I wrapped return data with custom class which will hold error message and type. Earlier I asked related question but got no good answer for my case where ViewModel is written natively in each platform.

If you find any problems with this approach or have any ideas please share.

Custom return data wrapper:

class Result<T>(
    val status: Status,
    val value: T? = null,
    val error: KError? = null
)

enum class Status {
    SUCCESS, FAIL
}

data class KError(
    val type: ErrorType,
    val message: String? = null,
)

enum class ErrorType {
    UNAUTHORIZED, CANCELED, OTHER
}

Custom interface

interface Cancelable {
    fun cancel()
}

Repository interface:

//Convert this code inside of Repository interface:

@Throws(Throwable::class)
suspend fun fetchData(): List<String>

//To this:

fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable

Repository implementation:

override fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable = runInsideOfCancelableCoroutine {
    val result = executeAndHandleExceptions {
        val data = networkExample()
        // do mapping, db operations, etc.
        data
    }

    action.invoke(result)
}

// example of doing heavy background work
private suspend fun networkExample(): List<String> {
    // delay, thread sleep
    return listOf("data 1", "data 2", "data 3")
}

// generic function for reuse
private fun runInsideOfCancelableCoroutine(task: suspend () -> Unit): Cancelable {

    val job = Job()

    CoroutineScope(Dispatchers.Main + job).launch {
        ensureActive()
        task.invoke()
    }

    return object : Cancelable {
        override fun cancel() {
            job.cancel()
        }
    }
}

// generic function for reuse
private suspend fun <T> executeAndHandleExceptions(action: suspend () -> T?): Result<T> {
    return try {
        val data = action.invoke()
        Result(status = Status.SUCCESS, value = data, error = null)
    } catch (t: Throwable) {
        Result(status = Status.FAIL, value = null, error = ErrorHandler.getError(t))
    }
}

ErrorHandler:

object ErrorHandler {

    fun getError(t: Throwable): KError {
        when (t) {
            is ClientRequestException -> {
                try {
                    when (t.response.status.value) {
                        401 -> return KError(ErrorType.UNAUTHORIZED)
                    }
                } catch (t: Throwable) {

                }
            }
            is CancellationException -> {
                return KError(ErrorType.CANCELED)
            }
        }
        return KError(ErrorType.OTHER, t.stackTraceToString())
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *