3

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?

Marat
  • 6,142
  • 6
  • 39
  • 67
  • you can use cancel operation – zeytin Jan 27 '21 at 21:11
  • @zeytin Could you please provide some example? When tried to find myself before I couldn't find any – Marat Jan 27 '21 at 21:18
  • sure i dropped, it was helpful ? – zeytin Jan 28 '21 at 19:04
  • Hello, @Marat, can you please tell me this question's status? – Artyom Degtyarev Mar 04 '21 at 10:35
  • @ArtyomDegtyarev I didn't find any existing solution, so I came up with my own. I posted it as an answer. Please check it out. – Marat Mar 04 '21 at 19:57
  • You could have your `ViewModels` in shared code with some sort of `onDetach` lifecycle method, and handle cancellation in that method. – enyciaa Mar 07 '21 at 23:54
  • I know is too late but in every VM, create a scope `protected val scope = MainScope(Dispatchers.Main, log)` and launch all your coroutines with it. When `onDetach`, call a function in your VM that does `scope.onDestroy()` to cancel all operations. – user3471194 May 14 '22 at 16:10

3 Answers3

1

Lets suppose that the object session is a URLSession instance, you can cancel it by:

session.invalidateAndCancel()
zeytin
  • 5,545
  • 4
  • 14
  • 38
  • thank you for your answer @zeytin. I'm afraid it is not going to be helpful. My question is about Kotlin Multiplatform project where network requests, background tasks are handled in data layer side and it is written in Kotlin. Maybe they get compiled to make use of iOS native tools like URLSession, but I don't have direct reference to it. I'm looking for something based on KMM – Marat Mar 04 '21 at 11:01
1

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())
    }
}
Marat
  • 6,142
  • 6
  • 39
  • 67
1

You probably have 3 options:

  • If you're using a some sort of reactive set up iOS side (e.g. MVVM) you could just choose to ignore cancellation. Cancellation will only save a minimal amount of work.

  • Wrap your iOS calls to shared code in an iOS reactive framework (e.g. combine) and handle cancellation using the iOS framework. The shared work would still be done, but the view won't be updated as your iOS framework is handling cancellation when leaving the screen.

  • Use Flow with this closable helper

enyciaa
  • 1,982
  • 1
  • 14
  • 24