12

Using coroutines for the first time. Need help.

Here is my flow:

Presenter wants to login so calls Repository Interface. Repository implements RepositoryInterface. So Repository calls APIInterface. APIInterface is implemented by APIInterfaceImpl. The APIInterfaceImpl finally calls the MyRetrofitInterface.

Here is the flow diagrammatically:

Presenter -> Repository -> APIInterfaceImpl -> MyRetrofitInterface

Once I get login response:

APIInterfaceImpl -> Repository -> Stores the data in cache -> Gives http status code to Presenter

Here is my code:

RepositoryInterface.kt

fun onUserLogin(loginRequest: LoginRequest): LoginResponse

Repository.kt

class Repository : RepositoryInterface {
   private var apiInterface: APIInterface? = null

   override fun onUserLogin(loginRequest: LoginRequest): LoginResponse {
         return apiInterface?.makeLoginCall(loginRequest)
   }
}

APIInterface.kt

suspend fun makeLoginCall(loginRequest): LoginResponse?

APIInterfaceImpl.kt

override suspend fun makeLoginCall(loginRequest: LoginRequest): LoginResponse? {
        if (isInternetPresent(context)) {
            try {
                val response = MyRetrofitInterface?.loginRequest(loginRequest)?.await()
                return response
            } catch (e: Exception) {
                //How do i return a status code here
            }
        } else {
        //How do i return no internet here
            return Exception(Constants.NO_INTERNET)
        }
}

MyRetrofitInterface.kt

@POST("login/....")
fun loginRequest(@Body loginRequest: LoginRequest): Deferred<LoginResponse>?

My questions are:

  1. Is my approach architecturally right?
  2. How do I pass http error codes or no internet connection in my code
  3. Any more nicer approach to my solution?
Sergio
  • 27,326
  • 8
  • 128
  • 149
Alessandra Maria
  • 245
  • 1
  • 3
  • 11

2 Answers2

15

It is a good practice to launch a coroutine in a local scope which can be implemented in a lifecycle aware classes, for example Presenter or ViewModel. You can use next approach to pass data:

  1. Create sealed Result class and its inheritors in separate file:

    sealed class Result<out T : Any>
    class Success<out T : Any>(val data: T) : Result<T>()
    class Error(val exception: Throwable, val message: String = exception.localizedMessage) : Result<Nothing>()
    
  2. Make onUserLogin function suspendable and returning Result in RepositoryInterface and Repository:

    suspend fun onUserLogin(loginRequest: LoginRequest): Result<LoginResponse> {
        return apiInterface.makeLoginCall(loginRequest)
    }
    
  3. Change makeLoginCall function in APIInterface and APIInterfaceImpl according to the following code:

    suspend fun makeLoginCall(loginRequest: LoginRequest): Result<LoginResponse> {
        if (isInternetPresent()) {
            try {
                val response = MyRetrofitInterface?.loginRequest(loginRequest)?.await()
                return Success(response)
            } catch (e: Exception) {
                return Error(e)
            }
        } else {
            return Error(Exception(Constants.NO_INTERNET))
        }
    }
    
  4. Use next code for your Presenter:

    class Presenter(private val repo: RepositoryInterface,
                    private val uiContext: CoroutineContext = Dispatchers.Main
    ) : CoroutineScope { // creating local scope
    
        private var job: Job = Job()
    
        // To use Dispatchers.Main (CoroutineDispatcher - runs and schedules coroutines) in Android add
        // implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'
        override val coroutineContext: CoroutineContext
            get() = uiContext + job
    
        fun detachView() {
            // cancel the job when view is detached
            job.cancel()
        }
    
        fun login() = launch { // launching a coroutine
            val request = LoginRequest()
            val result = repo.onUserLogin(request) // onUserLogin() function isn't blocking the Main Thread
    
            //use result, make UI updates
            when (result) {
                is Success<LoginResponse> -> { /* update UI when login success */ } 
                is Error -> { /* update UI when login error */ }
            }
        }
    }
    

EDIT

We can use extension functions on Result class to replace when expression:

inline fun <T : Any> Result<T>.onSuccess(action: (T) -> Unit): Result<T> {
    if (this is Success) action(data)
    return this
}
inline fun <T : Any> Result<T>.onError(action: (Error) -> Unit): Result<T> {
    if (this is Error) action(this)
    return this
}

class Presenter(...) : CoroutineScope {

    // ...

    fun login() = launch {
        val request = LoginRequest()
        val result = repo.onUserLogin(request) 

        result
            .onSuccess {/* update UI when login success */ }
            .onError { /* update UI when login error */ }
    }
}
Sergio
  • 27,326
  • 8
  • 128
  • 149
  • thank you for the example... can you please show me the code for repository and what should MyRetrofitInterface return? A Deferred or Result? – Alessandra Maria Jan 07 '19 at 18:30
  • `MyRetrofitInterface` is the same as in your code. Added Repository code to my answer – Sergio Jan 07 '19 at 18:53
  • Thank you...just a last question in my repository, I need to store the body of apiInterface.makeLoginCall(loginRequest). Since it returns a result, how could I get the body of it? – Alessandra Maria Jan 07 '19 at 19:05
  • You can do it like this: `if (result is Success) { val response = result.data }` – Sergio Jan 07 '19 at 19:20
  • @Sergey `login function isn't blocking the Main Thread when it is marked as 'suspend'` this is not true. suspending functions don't magically turn blocking code to unblocking code. In your example, the call to `repo.onUserLogin(request)` will block the main thread. In order to avoid it, you have to use a non-Main dispatcher. Since this is a web request, the recommended dispatcher is `Dispatchers.IO`. To change dispatcher, you should wrap the call with `val result = withContext(Dispatchers.IO) { repo.onUserLogin(request) }`. – Nimrod Dayan May 22 '19 at 20:17
  • in `makeLoginCall` function there is a call to `MyRetrofitInterface?.loginRequest(loginRequest)?.await()`, so it is already running on another thread and no need to use `withContext(Dispatchers.IO)`. `MyRetrofitInterface?.loginRequest(loginRequest)?` returns a Deferred object, usually its used with `await` suspending function in order to wait for the result without blocking the `main/current thread`. @NimrodDayan I've edited comment a little bit to avoid misunderstanding. – Sergio May 22 '19 at 20:31
  • `Deferred` type and its `await()` method is used for concurrency. By itself, it does not provide any means to "wait for the result without blocking". For example, `async { Thread.sleep(5000L) }` if run in the context of the Main dispatcher, will block the main thread! The only reason `repo.onUserLogin(request)` in your code won't block the main thread is because as you said Retrofit internally manages worker threads for the enqueued requests. But this is an implementation detail of Retrofit. It does not mean that any API that returns Deferred will do the same and that's what emphasizing here. – Nimrod Dayan May 25 '19 at 20:40
  • I am trying this pattern but when an error occurs it does not go into the is Error -> block even though the debugger is saying its an error $result = {Error@10966} – user3561494 Jun 14 '20 at 18:31
  • Thanks! What is `LoginRequest`? – CoolMind Jun 29 '20 at 08:34
  • I think it's some kind of data object, containing request information, e.g. email and password. – Sergio Jun 29 '20 at 09:39
6

EDIT:

I am trying this solution in my new app and i released that if an error occurs in launchSafe method and try to retry request, launcSafe() method does not work correctly. So i changed the logic like this and problem is fixed.

fun CoroutineScope.launchSafe(
    onError: (Throwable) -> Unit = {},
    onSuccess: suspend () -> Unit
) {
   launch {
        try {
            onSuccess()
        } catch (e: Exception) {
            onError(e)
        }
    }
}

OLD ANSWER:

I think a lot about this topic and came with a solution. I think this solution cleaner and easy to handle exceptions. First of all when use write code like

fun getNames() = launch { }  

You are returning job instance to ui i think this is not correct. Ui should not have reference to job instance. I tried below solution it's working good for me. But i want to discuss if any side effect can occur. Appreciate to see your comments.

fun main() {


    Presenter().getNames()

    Thread.sleep(1000000)

}


class Presenter(private val repository: Repository = Repository()) : CoroutineScope {

    private val job = Job()

    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Default // Can be Dispatchers.Main in Android

    fun getNames() = launchSafe(::handleLoginError) {
        println(repository.getNames())
    }
    

    private fun handleLoginError(throwable: Throwable) {
        println(throwable)
    }

    fun detach() = this.cancel()

}

class Repository {

    suspend fun getNames() = suspendCancellableCoroutine<List<String>> {
        val timer = Timer()

        it.invokeOnCancellation {
            timer.cancel()
        }

        timer.schedule(timerTask {
            it.resumeWithException(IllegalArgumentException())
            //it.resume(listOf("a", "b", "c", "d"))
        }, 500)
    }
}


fun CoroutineScope.launchSafe(
    onError: (Throwable) -> Unit = {},
    onSuccess: suspend () -> Unit
) {
    val handler = CoroutineExceptionHandler { _, throwable ->
        onError(throwable)
    }

    launch(handler) {
        onSuccess()
    }
}
Community
  • 1
  • 1
toffor
  • 1,219
  • 1
  • 12
  • 21
  • Have you moved from `CoroutineExceptionHandler` to `try-catch`? – CoolMind Jun 26 '20 at 15:01
  • 1
    Yes i moved, you should move definetly. Because first approach not best. – toffor Jun 27 '20 at 21:40
  • 1
    Thank you! I have been using `try-catch` for 2 years. I thought, `CoroutineExceptionHandler` would be better. – CoolMind Jun 28 '20 at 13:27
  • This is a good variant, but I found that we cannot get a right class name, method name, line number of a calling method. In `try-catch` we can invoke `Thread.currentThread().stackTrace[2]` and get line number of a crash line, but not of a calling class, but a class where `CoroutineScope.launchSafe` is written in. I mean, if we write this extension inside `MyLaunch.kt`, then in `try-catch` we will get `MyLaunch.launchSafe:10`, not `SomeFragment.loadItems:120`. – CoolMind Jun 30 '20 at 10:36
  • To overcome this behaviour, you can use `inline` modifier with `crossinline` and `noinline` modifiers. In this case we can capture a calling method, but not it's line number. – CoolMind Jun 30 '20 at 11:09
  • Thanks that's a good point to debug properly. I will try it. – toffor Jul 01 '20 at 06:52
  • Good luck! I will later write a similar solution in another topic! Thanks! – CoolMind Jul 01 '20 at 20:52