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?
2
  • where and how do you start a coroutine? Commented Jan 7, 2019 at 16:35
  • Yeah thats my problem...Can you please tell me how and where could i do that? Commented Jan 7, 2019 at 16:41

2 Answers 2

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 */ }
    }
}
Sign up to request clarification or add additional context in comments.

10 Comments

thank you for the example... can you please show me the code for repository and what should MyRetrofitInterface return? A Deferred or Result?
MyRetrofitInterface is the same as in your code. Added Repository code to my answer
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?
You can do it like this: if (result is Success<LoginResponse>) { val response = result.data }
@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) }.
|
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()
    }
}

7 Comments

Have you moved from CoroutineExceptionHandler to try-catch?
Yes i moved, you should move definetly. Because first approach not best.
Thank you! I have been using try-catch for 2 years. I thought, CoroutineExceptionHandler would be better.
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.
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.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.