Confusing about Kotlin Coroutines execution: where to call await()? - android

I read a lot of docs about Kotlin coroutines but still having some doubts. I'm using Retrofit with coroutines so I need to do request with Dispatchers.IO context but use result within Dispatchers.Main context to assign it to ViewModel. My code is:
fun doHttpreq() {
viewModelScope.launch(Dispatchers.IO) {
try {
//should I call await() here? (I guess the correct way to keep execution of request outside of Main thread)
val request = RestClient.instance.getItems().await()
withContext(Dispatchers.Main) {
//or should I call await() here? (BUT need request to be executed outside of Main thread!)
if (request.isSuccessful) {
//asign items to ViewModel
} else {
//asign error to ViewModel
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
//asign error to ViewModel
}
}
}
}

You can take your deffered job in variable and then await it on your Main dispatcher like below :
try {
//Rather than await here, you take your Job as Deffered
val request: Deferred? = RestClient.instance.getItems()
withContext(Dispatchers.Main) {
//Yes, you can await here because it's non-blocking call and can be safely obtained from here once completed
val result = request?.await()
if (request.isSuccessful) {
//asign items to ViewModel
} else {
//asign error to ViewModel
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
//asign error to ViewModel
}
}
What official doc states about await() :
Awaits for completion of this value without blocking a thread and resumes when deferred computation is complete, returning the resulting value or throwing the corresponding exception if the deferred was cancelled.
This suspending function is cancellable. If the Job of the current coroutine is cancelled or completed while this suspending function is waiting, this function immediately resumes with CancellationException.
This function can be used in select invocation with onAwait clause. Use isCompleted to check for completion of this deferred value without waiting.

As Coroutines are suspending instead of blocking, there should not be any need to manage the thread they are running on. In your case Retrofit handles this for you. Also the Deferred type is actually a hot data source. This means that the Call is executed before you even call await on it. await just waits for the data to be there.
So instead you can launch on the Main dispatcher directly. Therefore you only have one place to call await() from.
viewModelScope.launch(Dispatchers.Main) {
try {
val request = RestClient.instance.getItems().await()
if (request.isSuccessful) {
//asign items to ViewModel
} else {
//asign error to ViewModel
}
} catch (e: Exception) {
//asign error to ViewModel
}
}

Related

Is it necessary to use return withContext(Dispatchers.IO)?

I'm using Firebase authentication. In the repository I have this function:
override suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
This function is called from within the ViewModel class:
var response by mutableStateOf<Result<Boolean>>(Result.Success(false))
private set
fun signIn() = viewModelScope.launch {
response = repository.signIn()
}
Which works fine but I was suggested to use in the ViewModel:
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
response = repository.signIn()
}
To add Dispatchers.IO and inside the repository:
override suspend fun signIn(): Result<Boolean> {
return withContext(Dispatchers.IO) {
try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
}
To launch a coroutine using withContext. I and I don't understand why? I'm using Jetpack Compose.
Whoever suggested changing your code is wrong.
It is a general Kotlin coroutines convention that suspend functions never need to be called on a specific dispatcher, specifically because they must never block. They always internally delegate to a specific dispatcher if they need one. (But perhaps as an optimization, a private suspend function might avoid doing it for a function that must be called on the Main dispatcher.)
Since this is a convention, all the libraries from Google, Android, Square, etc. and anyone else who knows what they're doing, only have suspend functions that can be called from any dispatcher.
This includes the await() call you're using with Firebase. Therefore, your repository's signIn function is already perfectly fine as-is. Since it doesn't call any blocking functions, and the suspend function it calls is a proper suspend function that also does not block, it conforms to the standard (it doesn't block).
The function in your ViewModel is also fine. No dispatcher needs to be specified.
Actually, since you are already calling signIn from a coroutine started with Dispatchers.IO you don't have to use return withContext(...).
Since your repository method is suspend, it is able to call coroutines without special blocks like withContext.
// This line tells to launch code on separate IO thread, to avoid UI freezing
// Since default viewModelScope.launch runs on Dispatchers.Main, which is
// also used for rendering
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
response = repository.signIn()
}
In your repository you can just
// Since signIn was called on IO context from viewModel, it will also
// return on IO
override suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
We have a two ways to start coroutine async and launch.
launch will use to perform serial/sequence task in background.
async is used when we expect some result back and also want to perform parallel operation.
Same way withContext is nothing but another way of writing the async where one does not have to write await(). When withContext, is used, it runs the tasks in series instead of parallel. So one should remember that when we have a single task in the background and want to get back the result of that task, we should use withContext.
In your case you can change your code as below
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
val response = async { repository.signIn()}.await()
}
and remove withContext
suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
One more way if you don't want to use return with withContext
override suspend fun signIn() = {
withContext(Dispatchers.IO) {
try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
}
In Nutshell if you expecting some result from your task then you have to use async or withContext.
Hope I am able to solve your problem or issue.

Kotlin Coroutines Flow catch mechanism

In my sample I'm calling network operation and emitting success case but on error e.g 404 app crashes wihout emitting exception. Surrendering with try catch prevent crashes but I want to pass error till the ui layer like success case.
suspend fun execute(
params: Params,
):
Flow<Result<Type>> = withContext(Dispatchers.IO) {
flow {
emit(Result.success(run(params)))
}.catch {
emit(Result.failure(it))
}
}
There is a helpful function runCatching for creating a Result easily, but the problem in coroutines is that you don't want to be swallowing CancellationExceptions. So below, I'm using runCatchingCancellable from my answer here.
This shouldn't be a Flow since it returns a single item.
If run is a not a blocking function (it shouldn't be if you are using Retrofit with suspend functions), your code can simply be:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
run(params)
}
If it is a blocking function you can use:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
withContext(Dispatchers.IO) {
run(params)
}
}
If you were going to return a Flow (which you shouldn't for a returning a single item!!), then you shouldn't make this a suspend function, and you should catch the error inside the flow builder lambda:
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
run(params)
})
}
// or if run is blocking (it shouldn't be):
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
withContext(Dispatchers.IO) { run(params) }
})
}
If you want to use flows you can use the catch method of flows.
As you said you can use try-catch but it would break the structured concurrency since it would catch the cancellation exception as well or it would avoid the cancellation exception to be thrown.
One thing that you can do is to use an Exception handler at the point where you launch the root coroutine that calls the suspend function.
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
// handle it
}
scope.launch(handler) { // root coroutine
execute(params)
somethingThatShouldBeExecutedOnlyIfPreviousCallDoesNotThrow()
}
This solution is good for both flows and non-flow coroutines.
In the solution with the runCatching you will have to manually check the result of the first execute to avoid the second one to run.
One interesting thread is here.

My function is asynchronous, why do I get a NetworkOnMainThread Exception?

This is the object where all my API calls are made. Sometimes when I make a call to this function, I receive a NetworkOnMainThread exception. It doesn't happen every time. I'm confused because I've made this function asynchronous... why am I still getting this exception?
object APICaller{
private const val apiKey = "API_KEY_HERE"
//Live Data Objects
var errorCode = MutableLiveData<Int>()
var fetchedResponse = MutableLiveData<Response>()
//Asynchronous network call
suspend fun networkCall(query: String) = withContext(Dispatchers.Default){
val apiURL = "API_URL_HERE"
try{
//Get response
val response = OkHttpClient().newCall(Request.Builder().url(apiURL).build()).execute()
if(response.isSuccessful){
//UI changes (including changes to LiveData values) must be performed on main thread.
Handler(Looper.getMainLooper()).post{
fetchedResponse.value = response
}.also{
Log.i("Response Succ", response.toString())
}
} else {
Handler(Looper.getMainLooper()).post{
errorCode.value =
ToastGenerator.REQUEST_ERROR
}.also{
Log.i("Response Fail", response.toString())
}
}
//Catch any thrown network exceptions whilst attempting to contact API
} catch(e: Exception){
Handler(Looper.getMainLooper()).post{
errorCode.value =
ToastGenerator.NETWORK_ERROR
}.also{
Log.i("Network Fail", e.message.toString())
}
}
}
}
There are three other classes that make utilise return value from the APICaller networkCall() function. The first is a ViewModel that references it directly.
class BrowseViewModel: ViewModel() {
//LiveData Objects
//Transformations listen to LiveData in APICaller and map it to LiveData in this ViewModel
var errorCode: LiveData<Int>? = Transformations.map(APICaller.errorCode){ code ->
return#map code
}
var obtainedResponse: LiveData<String> = Transformations.map(APICaller.fetchedResponse){ response ->
return#map response.body()?.string()
}
//Upon a search request, make a network call
fun request(query: String) {
GlobalScope.launch{
APICaller.networkCall(query)
}
}
//Convert API response to GameData object
fun handleJSONString(jsonString: String, file: String) : List<GameData>{
return DataTransformer.JSONToGameData(JSONObject(jsonString), JSONObject(file))
}
}
The second is an fragment that calls the ViewModel's function.
fun request(query: String){
browseViewModel.request(query)
progressSpinner?.visibility = View.VISIBLE
}
The third is an Activity that calls upon the Fragment's function.
private fun makeRequest(query: String){
browseFragment.let{
supportFragmentManager.beginTransaction().replace(R.id.fragmentContainer, it).commit()
it.request(query)
}
}
Could it be related to these other functions?
Any feedback is greatly appreciated. Thank you in advance :)
I don't see exactly what could go wrong in your code, but it could definitely be much cleaner, which possibly would resolve your issue, or at least make it easier to find the error.
A good strategy with suspend functions is to design them to always be called from a Main dispatcher. Then you can freely make UI calls in them and wrap only the background parts with withContext. So your above function could move the withContext() down to only wrap the execute() call, and all your Handler usage could be removed. (Incidentally, those would have been cleaner using withContext(Dispatchers.Main).)
However, Retrofit already provides a suspend function version of making a call, so you don't even need the withContext wrapper. Just use await() instead of execute() and your suspend function would collapse down to:
suspend fun networkCall(query: String) {
val apiURL = "API_URL_HERE"
try{
val response = OkHttpClient().newCall(Request.Builder().url(apiURL).build()).await()
if (response.isSuccessful){
Log.i("Response Succ", response.toString())
fetchedResponse.value = response
} else {
Log.i("Response Fail", response.toString())
errorCode.value = ToastGenerator.REQUEST_ERROR
}
} catch(e: Exception){
Log.i("Network Fail", e.message.toString())
errorCode.value = ToastGenerator.NETWORK_ERROR
}
}
And then instead of using GlobalScope, you should use a viewModelScope or lifecycleScope so your coroutines don't leak UI components. And the await() suspending function above supports cancellation, so if for instance your ViewModel is destroyed due to the associated Fragment or Activity going out of scope, your network call will be cancelled automatically for you.
fun request(query: String) {
viewModelScope.launch {
APICaller.networkCall(query)
}
}
If the problem persists, study your stack trace closely to see where you might be making the error.

Make part of coroutine continue past cancellation

I have a file managing class that can save a big file. The file manager class is an application singleton, so it outlives my UI classes. My Activity/Fragment can call the save suspend function of the file manager from a coroutine and then show success or failure in the UI. For example:
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
try {
myFileManager.saveBigFile()
myTextView.text = "Successfully saved file"
} catch (e: IOException) {
myTextView.text = "Failed to save file"
}
}
//In MyFileManager
suspend fun saveBigFile() {
//Set up the parameters
//...
withContext(Dispatchers.IO) {
//Save the file
//...
}
}
The problem with this approach is that I don't want the save operation to be aborted if the Activity is finished. If the activity is destroyed before the withContext block gets going, or if the withContext block has any suspension points in it, then saving will not be completed because the coroutine will be canceled.
What I want to happen is that the file is always saved. If the Activity is still around, then we can show UI updates on completion.
I thought one way to do it might be to start a new coroutineScope from the suspend function like this, but this scope still seems to get cancelled when its parent job is cancelled.
suspend fun saveBigFile() = coroutineScope {
//...
}
I thought another alternative might be to make this a regular function that updates some LiveData when it's finished. The Activity could observe the live data for the result, and since LiveData automatically removes lifecycle observers when they're destroyed, the Activity is not leaked to the FileManager. I'd like to avoid this pattern if the something less convoluted like the above can be done instead.
//In MyActivity:
private fun saveTheFile() {
val result = myFileManager.saveBigFile()
result.observe(this#MyActivity) {
myTextView.text = when (it) {
true -> "Successfully saved file"
else -> "Failed to save file"
}
}
}
//In MyFileManager
fun saveBigFile(): LiveData<Boolean> {
//Set up the parameters
//...
val liveData = MutableLiveData<Boolean>()
MainScope().launch {
val success = withContext(Dispatchers.IO) {
//Save the file
//...
}
liveData.value = success
}
return liveData
}
You can wrap the bit that you don't want to be cancelled with NonCancellable.
// May cancel here.
withContext(Dispatchers.IO + NonCancellable) {
// Will complete, even if cancelled.
}
// May cancel here.
If you have code whose lifetime is scoped to the lifetime of the whole application, then this is a use case for the GlobalScope. However, just saying GlobalScope.launch is not a good strategy because you could launch several concurrent file operations that may be in conflict (this depends on your app's details). The recommended way is to use a globally-scoped actor, in the role of an executor service.
Basically, you can say
#ObsoleteCoroutinesApi
val executor = GlobalScope.actor<() -> Unit>(Dispatchers.IO) {
for (task in channel) {
task()
}
}
And use it like this:
private fun saveTheFile() = lifecycleScope.launch {
executor.send {
try {
myFileManager.saveBigFile()
withContext(Main) {
myTextView.text = "Successfully saved file"
}
} catch (e: IOException) {
withContext(Main) {
myTextView.text = "Failed to save file"
}
}
}
}
Note that this is still not a great solution, it retains myTextView beyond its lifetime. Decoupling the UI notifications from the view is another topic, though.
actor is labeled as "obsolete coroutines API", but that's just an advance notice that it will be replaced with a more powerful alternative in a future version of Kotlin. It doesn't mean it's broken or unsupported.
I tried this, and it appears to do what I described that I wanted. The FileManager class has its own scope, though I suppose it could also be GlobalScope since it's a singleton class.
We launch a new job in its own scope from the coroutine. This is done from a separate function to remove any ambiguity about the scope of the job. I use async
for this other job so I can bubble up exceptions that the UI should respond to.
Then after launch, we await the async job back in the original scope. await() suspends until the job is completed and passes along any throws (in my case I want IOExceptions to bubble up for the UI to show an error message). So if the original scope is cancelled, its coroutine never waits for the result, but the launched job keeps rolling along until it completes normally. Any exceptions that we want to ensure are always handled should be handled within the async function. Otherwise, they won't bubble up if the original job is cancelled.
//In MyActivity:
private fun saveTheFile() = lifecycleScope.launch {
try {
myFileManager.saveBigFile()
myTextView.text = "Successfully saved file"
} catch (e: IOException) {
myTextView.text = "Failed to save file"
}
}
class MyFileManager private constructor(app: Application):
CoroutineScope by MainScope() {
suspend fun saveBigFile() {
//Set up the parameters
//...
val deferred = saveBigFileAsync()
deferred.await()
}
private fun saveBigFileAsync() = async(Dispatchers.IO) {
//Save the file
//...
}
}

Switching to UI context in coroutines

I'm new to coroutines and I'm wondering if it's possible to switch from coroutineScope (GlobalScope) to UI scope for the code below. My problem is that the steps inside the coroutine launch body must be executed in a worker thread, otherwise the listener notification must be executed in the ui thread in order to avoid to call runOnUiThread in my activity code.
override suspend fun startRent(name: String, bikeMode: BikeMode, listener: StartRentListener) {
var bleDevice : RxBleDevice
val scanFilter: ScanFilter = ScanFilter.Builder().setDeviceName(name).build()
val scanSettings: ScanSettings = ScanSettings.Builder().build()
val job = GlobalScope.launch {
try {
bleDevice = rxBleClient.scanBleDevicesExt(rxBleClient, scanSettings, scanFilter)
val bleConnection = bleDevice.establishConnectionExt()
// write handshake
connectionManager.writeHandshake(bleDevice, bleConnection)
// open lock
openLock(bleDevice, bikeMode, bleConnection)
// getting user position
apiHelper.sendLockRequest(bleDevice.name, getPosition())
bleDevice.disconnect()
// this should be called on main thread once all the previous operations are finished
listener.onSuccess()
} catch (e: Exception) {
listener.onError(e)
}
}
job.join()
}
A snippet of my current activity code:
bikeAccessClient.startRent(bikeBLEName, BikeMode.HYBRID, object :
StartRentListener {
override fun onSuccess() {
runOnUiThread {
// UI update here
}
}
You may use withContext(Dispatchers.Main) {..} function to execute a part of your code with the other Coroutine Dispatcher.
kotlinx.coroutines.android contains the definition of the Dispatchers.Main function and it integrates correctly with Android UI.
Using explicit Dispatcher in your code is quite error-prone. Instead, I would recommend designing the code with fewer explicit requirements.
I would wrote something like that:
fun uiActionHandlerToStartTheProcess() {
launch(Dispatchers.Main) {
val result = startRent(...) // no callback here, suspend function
//UI Update Here
}
}
suspend fun CoroutineScope.startRent() : SomeResultOfWork {
//that function offloads the execution to a IO (aka brackground) thread
return withContext(Dispatchers.IO){
//here goes your code from `startRent`
//use `suspendCancellableCoroutine {cont -> .. }` if you need to handle callbacks from it
SomeResultOfWork()
}
The code in the launch(Dispatchers.Main){..} block is executed in the UI thread. The call to startRent suspend function suspends the execution in the UI thread. Once the startRent is ready with the reply (from a background thread) it resumes the execution (which is done by the Dispatchers.Main and equivalent to the runOnUiThread {...}) and executes the UI update from the right thread

Categories

Resources