imin
imin

Reputation: 4578

calling coroutine function from activity returns `should be called only from a coroutine or another suspend function`

I'm trying to use coroutines in my code since in one of my function, I need to do multiple network calls and wait for its result. Below is a portion of my activity code:

class SellerDeliveryRegister : AppCompatActivity() {

    lateinit var sellerDeliveryVM:SellerDeliveryVM

    sellerDeliveryVM = ViewModelProviders.of(this).get(SellerDeliveryVM::class.java)

    var uploadVehicleImageResult = sellerDeliveryVM.uploadVehiclesImages(uploadMotorImage1Url,uploadMotorImage2Url)
}

And below is a portion of my sellerDeliveryVM ViewModel code:

class SellerDeliveryVM: ViewModel() {
    suspend fun uploadVehiclesImages(uploadMotorImage1Url: String, uploadMotorImage2Url: String): Unit = withContext(Dispatchers.IO){

        var uploadMotorImage1Result = "-1"; var uploadMotorImage2Result = "-1"; 
        viewModelScope.launch(Dispatchers.IO) {

            uploadMotorImage1Result = withContext(Dispatchers.Default) {
                NetworkRepository.instance!!.uploadFile(uploadMotorImage1Url)
            }

            uploadMotorImage2Result = withContext(Dispatchers.Default) {
                NetworkRepository.instance!!.uploadFile(uploadMotorImage2Url)
            }

            return@launch;
        }
        return@withContext
    }
}

Please take note that previously uploadVehiclesImages is a 'normal' function that doesn't use coroutine, so now I'm converting it to use coroutine.

Below are the problems I'm facing:

  1. Line var uploadVehicleImageResult = sellerDeliveryVM.uploadVehiclesImages(uploadMotorImage1Url,uploadMotorImage2Url) inside my SellerDeliveryRegister class returns this error:

Suspend function 'uploadVehiclesImages' should be called only from a coroutine or another suspend function

  1. Initially I want to return Boolean from uploadVehiclesImages, so I have return true in place of the return@launch and return false in place of the return@withContext, but then I will get the error return is not allowed here, and Android Studio suggested me to make the changes above, although I really have no idea what the changes meant there.

So what should I do to fix this problem 1, and can anyone enlighten me more on what's really happening on the problem 2?

Upvotes: 1

Views: 1667

Answers (2)

Tenfour04
Tenfour04

Reputation: 93659

For part 1, you cannot use a coroutine to initialize a property. Coroutines return some time in the future, but properties have to be initialized immediately at class instantiation time. You'll have to change the strategy so you launch a coroutine that calls the suspend function, and then does something with the result when it arrives.

For part 2, you have an awkwardly composed suspend function. A proper suspend function typically isn't launching other coroutines unless it is using them to break down multiple simultaneous asynchronous actions and then waiting for them.

The convention for a suspend function is that it is safe to call from any Dispatcher. It's not proper to be sending off these background actions by launching a coroutine in a specific coroutine scope. Usually, a coroutine that calls a suspend function should not have to worry that the suspend function is going to launch some other coroutine in another scope, because this breaks support for cancellation.

Also, you can use async instead of launch to run suspend functions that you need a result from. That will avoid the awkward variables you've created to store the results (and you neglected to wait for).

Assuming you want to return both of these image results, you'll have to wrap them in another class, such as List. So your function could look like below. It returns something, not Unit. It uses aysnc to run the two requests simultaneously.

suspend fun uploadVehiclesImages(uploadMotorImage1Url: String, uploadMotorImage2Url: String): List<ImageUploadResult> {
    return listOf(uploadMotorImage1Url, uploadMotorImage2Url)
        .map { aysnc { NetworkRepository.instance!!.uploadFile(it) }
        .awaitAll()
}

I just put ImageUploadResult to stand in for whatever this uploadFile function returns. Maybe it's just Boolean.

Whenever you do want to call it, you would use either lifecycleScope (from an Activity or Fragment) or viewModelScope (from a ViewModel) to launch a coroutine that calls it. For example:

fun doUpload(url1: String, url2: String) {
    lifecycleScope.launch {
        val results = uploadVehiclesImages(url1, url2)
        // do something with results here
    }
}

Upvotes: 1

CommonsWare
CommonsWare

Reputation: 1006944

So what should I do to fix this problem 1

Remove the property. uploadVehiclesImages() returns Unit; there is no value in having Unit in a property. If your objective is to call uploadVehiclesImages() when the viewmodel is created, put a call to it in an init block, wrapped in a suitable coroutine launcher (e.g., viewModelScope.launch {}).

This assumes that you are going to keep the function in its current form — your next question suggests that this function may not be the right solution.

Initially I want to return Boolean from uploadVehiclesImages,

More importantly, you seem to want it to return values more than once. That is not how functions work in Kotlin. One call to uploadVehiclesImages() can return one Boolean value, but not one now and one sometime in the future.

If you want to be emitting a stream of results, a suspend function on its own is not the correct solution. For example, you could:

  • Use LiveData, with the suspend function updating the backing MutableLiveData, or

  • Use a function that returns a StateFlow or SharedFlow

Upvotes: 1

Related Questions