viewModelScope.launch and main thread ANR issue?

239 views Asked by At

As per As per Android Developer doc

viewModelScope.launch Create a new coroutine on the UI thread and code is as below

class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {

fun login(username: String, token: String) {

    // Create a new coroutine on the UI thread
    viewModelScope.launch {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"

        // Make the network call and suspend execution until it finishes
        val result = loginRepository.makeLoginRequest(jsonBody)

        // Display result of the network request to the user
        when (result) {
            is Result.Success<LoginResponse> -> // Happy path
            else -> // Show error in UI
        }
    }
}
}

Does it means if I run a network call inside viewModelScope.launch like a retrofit call will it cause an ANR issue or freeze the UI?

3

There are 3 answers

0
Steyrix On

Retrofit ensures that network call from loginRepository.makeLoginRequest() is performed via suspending function, which will not block the UI thread while waiting for response from the server. So there will be no ANR.

However, a good practice is to dispatch network calls to separate thread/context in place (in your case, in the implementation of LoginRepository). This will guarantee that you don't have to worry about network call being executed on a wrong thread. Only data layer will take the responsibility of dispatching the work to the right thread/context.

You can switch context or even launch coroutine on a different dispatcher.

suspend fun makeLoginRequest(jsonBody: String): Result<LoginResponse>{
    withContext(Dispatchers.IO) {... //perform network call on IO thread pool}
}

[UPD] The example above is actually referred to the official documentation.

We consider a function main-safe when it doesn't block UI updates on the main thread. The makeLoginRequest function is not main-safe, as calling makeLoginRequest from the main thread does block the UI. Use the withContext() function from the coroutines library to move the execution of a coroutine to a different thread.

And for clearance, it works as follows:

Within the coroutine, the call to loginRepository.makeLoginRequest() now suspends further execution of the coroutine until the withContext block in makeLoginRequest() finishes running.

Once the withContext block finishes, the coroutine in login() resumes execution on the main thread with the result of the network request.

0
therock24 On

First, make sure that the function makeLoginRequest is a suspend function.

Second, make sure that the code you are calling inside of it is actually suspendable.

If you are using Retrofit, the request interface is already implemented as a suspend function by default, so it is safe to use it.

Let's say that you were using OkHttp or any different library that still does some heavy task and does not implement a suspend function.

[UPDATE] The reference documentation is actually the link you posted in the question, but just down below "Use Coroutines for Main Safety" and the example below is based on it. Find it here

In this case, the safest way to do it would be something like this:

suspend fun makeLoginRequest(jsonBody: String): Result {
    return withContext(Dispatchers.IO) {
        // Create and execute the OkHttp request
        val client = OkHttpClient()
        val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())

        val request = Request.Builder()
            .url("http://api.service.com/login")
            .post(requestBody)
            .build()

        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) throw IOException("Unexpected code $response")

            // Process and return the response
            processResponse(response)
        }
    }
}

In this case, withContext guarantees that the code inside of this block is executed in an IO thread, which is appropriate for this kind of operations and the execution becomes suspended (not blocking) until the operation terminates. Only after is the function returning and the execution continues on the viewModelScope.

This is a simple way to convert non-suspending code to suspending.

0
AnumShafiq On

When I tried the viewModel.launch {} with Room fetch request inside using the suspend function it generates an ANR issue and the app crashes then I used the following code to make sure the long task doesn't suspend the main thread

 viewModelScope.launch {withContext(Dispatchers.IO) {}}