CoroutineScope vs suspend func - android

I'm a little confused.
I know that if a function wants to work with coroutines, it should be declared as suspend
For example:
private suspend fun doSomething() {
withContext(Dispatchers.IO) {
//do something
} }
And I also know that there is such a way to use coroutines without the function being suspend.
like:
private fun doSomething1() {
CoroutineScope(Dispatchers.IO).launch {
//do something
} }
What is the difference between the two functions?
When to use the first example and when to use the second example?

What is the difference between the two functions?
There are 2 major differences between the 2:
the usage is different: the suspend one "feels" synchronous, while the launch is explicitly asynchronous
the second function breaks structured concurrency, and shouldn't be written this way
Let me elaborate.
The suspend function appears synchronous from the usage perspective: when you call it, the next line of code is only executed when the function is done (like with any other regular function). This makes it easy to reason about. You can even assign the return value of a suspend function to a variable, and go on with your life as if the function wasn't suspend. That is, when you're in a suspend context already of course. When you're not, you have to start the "root" coroutine with an explicit coroutine builder (like launch, async or runBlocking).
When using launch, you're explicitly starting an asynchronous task, and thus the code after launch runs concurrently with what's inside the launch. So in turn, when calling doSomething1(), the code after it will run concurrently with whatever is in the launch inside. However, it is really not clear from the API's perspective that this function will launch a task that outlives it. This also goes with the fact that you shouldn't create "free" coroutine scopes like this. I'll elaborate below.
When to use the first example and when to use the second example?
Use suspend functions as much as possible to keep things simple. Most of the time, you don't need to start tasks that outlive the function call, so this is perfectly fine. You can still do some work concurrently inside your suspend function by using coroutineScope { ... } to launch some coroutines. This doesn't require an externally-provided scope, and all the computation will happen within the suspend function call from the caller's perspective, because coroutineScope {} will wait for the child coroutines to complete before it returns.
The function using launch as written here is very poorly behaved, you should never write things like this:
CoroutineScopes should not be created on the spot and left for dead. You should keep a handle on it and cancel it when appropriate
if you're already in the suspending world when calling this function, the existing coroutine context and jobs will be ignored
To avoid these problems, you can make the API explicit by making the CoroutineScope a receiver instead of creating one on the spot:
private fun CoroutineScope.doSomething1() {
launch(Dispatchers.IO) {
//do something
}
}
But only use this approach if the essence of the function is to start something that will keep going after the function returns.

The shortest answer is that suspend function is a block that can be executed in CoroutineScope. So it's not the first example vs the second example.
By combining those blocks you can start your own scope, and execute suspend functions using different contexts.
private suspend fun doSomething() {
withContext(Dispatchers.IO){
// task executed in io thread
}
}
private suspend fun doSomethingUI() {
withContext(Dispatchers.Main) {
// task executed in ui thread
}
}
private fun ioOperation() {
CoroutineScope(Dispatchers.IO).launch {
doSomething()
doSomethingUI()
}
}
Edit: This is just a basic sample made with simplicity in mind. It doesn't handle the proper lifecycle of the Coroutine Scope, and should not be directly used.

Related

How this change from runBlocking to sharedFlow work?

I would like to ask you why does it work?
Normally when I used collectLatest with flow my data wasn't collected on time and the return value was empty. I have to use async-await coroutines, but I have read it blocks main thread, so it is not efficient. I've made my research and find the solution using sharedflow.
Previously:
suspend fun getList: List<Items> {
CoroutineScope(Dispatchers.Main).launch {
async {
flow.collectLatest {
myItems = it
}
}.await()
}
return myItems
}
or without await-async and it returns emptyList
now:
suspend fun getList: List<Items> {
val sharedFlow = flow.conflate().shareIn(
coroutineScopeIO,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
return sharedFlow.first()
}
conflate means:
Conflates flow emissions via conflated channel and runs collector in a separate coroutine. The effect of this is that emitter is never suspended due to a slow collector, but collector always gets the most recent value emitted.
I'm not sure I understand it clearly. When I conflate flow, I just create seperate coroutine to emit what will be inside my another function as in my example shareIn().first() and using this variablesharedFlow which is suspended so will give the same effect I made asnyc-await, but in that case I do not block main thread, but only my exact *parentCoroutine-or-suspendFunction?
SharingStarted.WhileSubscribed()
It just means to start emit when subcribed.
conflate() has nothing to do with why this is working. The separate coroutine it talks about is run under the hood and you don't need to think about it. It's just to make sure your flow never causes the upstream emitter to have to wait for a slow collector, and your collector skips values if they are coming faster than it can handle them. conflate() makes it safe to have a slow collector without a buffer.
In your first code block, you are launching a new coroutine in a new CoroutineScope, so it is not a child coroutine and will not be waited for before the function returns. (Incidentally, this new coroutine will only finish when the Flow completes, and most types of Flows never complete.)
In the second code block, you are calling first() on the Flow, which suspends and gets the next value emitted by the flow and then returns that value without waiting for the Flow to complete.
Some other notes:
You should never use async { /*...*/ }.await() where await() is called immediately on the Deferred, because it is just a more convoluted version of withContext(/*...*/) { /*...*/ }.
It's a code smell to create a CoroutineScope that you never assign to a property, because the point of creating a scope is so you can manage the scope, and you obviously aren't managing it if you have no reference to it to work with.
You said you are worried about blocking the main thread, but nothing in the code you showed looks suspicious of blocking the main thread. But it's possible your flow that you are basing this on has blocking code in it. By convention it shouldn't. If that flow blocks, you should use the flowOn(Dispatchers.IO) operator on it at the source so downstream users don't have to worry about it.
Although your code worked, it doesn't make sense to create a SharedFlow in a function and immediately collect from it. It's not being shared with anything! Your code could be simplified to this equivalent code:
suspend fun getList: List<Items> {
return flow.first()
}

Why use suspend functions in kotlin coroutine?

Why we should use suspend?
Are they used only to restrict a function not to be used outside coroutine scope or other suspend functions?
Are they used only to restrict a function not to be used outside co routine scope or other suspend functions?
No. The main purpose of a suspend function is to suspend a coroutine it is called in to eliminate callback hell. Consider the following code with a callback:
fun interface Callback {
fun call()
}
fun executeRequest(c: Callback) {
// ... create new Thread to execute some request and call callback after the request is executed
c.call()
}
On the caller side there will be:
executeRequest {
// ... do sth after request is competed.
}
Imagine you need to make another request after that one:
executeRequest {
// make another request
executeRequest {
// and another one
executeRequest {
// and another one ...
executeRequest {
}
}
}
}
That is a Callback Hell. To avoid it we can get rid of a Callback code and use only suspend functions:
// withContext(Dispatchers.IO) switches a coroutine's context to background thread
suspend fun executeRequest() = withContext(Dispatchers.IO) {
// just execute request, don't create another Thread, it is already in the background Thread
// if there is some result, return it
}
And on the caller side if there are a couple of requests, that should be executed one by one, we can call them without the Callback Hell by launching a coroutine and calling those request in the coroutine:
viewModelScope.launch {
executeRequest()
executeRequest()
executeRequest()
}
If you wonder what it does, I guess you will find the answer in this near-duplicate question's answer.
It basically allows to use the syntax of synchronous calls to call an asynchronous function.
If the question is "Why use async programming?", then there are probably plenty of resources explaining this on the internet. It usually allows to use threads more effectively and avoids using too many resources.
Now as to why you would want to use suspend to do that instead of callbacks, there is probably several reasons. Probably the biggest of them is to avoid the infamous "callback hell" (well known in JS): using actual callbacks creates nesting in the code. It makes the language harder to manipulate because you can't use local variables or loops as easily as with regular sequential code. With suspend functions, the code reads sequentially even though some asynchronous mechanisms are used behind the scenes to resume executing a piece of code at a later point.
Another big reason to use suspend (and more precisely the coroutines library in general) is structured concurrency. It allows to organize your asynchronous work so you don't leak anything.
Under the hood, suspend functions provide the extra functionality (involving a hidden Continuation object) that allow their code to be stopped and resumed later from where it left off, instead of the typical limitation that all function calls in a function are called in sequence and block the thread the entire time. The keyword is there to make the syntax very simple for doing a long-running task in the background.
Three different benefits of this:
No need for callbacks for long-running tasks ("callback hell")
Without suspend functions, you would have to use callbacks to prevent a long-running task from blocking the thread, and this leads to complicated-looking code that is not written in sequential order, especially when you need to do a number of long-running tasks in sequence and even more-so if you need to do them in parallel.
Structured concurrency
Built-in features for automatically cancelling long-running tasks. Guarantees about shared state being correct whenever accessed despite thread switching.
No need for state machines for lazy iterators
A limited set of suspend functions can be used in lazy iterator functions like the iterator { } and sequence { } builders. These are a very different kind of coroutine than the ones launched with CoroutineScopes, because they don't do anything with thread switching. The yield() suspend function in these builders' lambdas allows the code to be paused until the next item is requested in iteration. No thread is being blocked, but not because it's doing some task in the background. It instead enables code to be written sequentially and concisely without a complicated state machine.

Getting mixed messages about coroutines

I've been trying to wrap my head around coroutines and I believe I have a fairly good understanding now, but there's still a couple things that aren't clear which I have questions about.
Imagine I have the below code:
lateinit var item: Int
val coroutine = globalScope.launch {
item = onePlusOne()
}
suspend fun onePlusOne(): Int {
return 1+1
}
When the suspend function onePlusOne is called, it doesn't start a new coroutine correct? It is still executing in the same one started by launch?
When the suspend function onePlusOne is called, it doesn't automatically suspend the coroutine that launch created right? If it does, how does it still execute the return 1+1 statement?
When the suspend function onePlusOne is called, it doesn't start a new coroutine correct? It is still executing in the same one started by launch?
Yes, a coroutine is only created when you create one from CoroutineScope.launch or CoroutineScope.async, withContext or runBlocking.
A suspend function does not create a new coroutine, but can suspend the caller if it calls any suspend function from inside it (it can be change of thread/dispatcher using withContext and waiting for the result, it can be a delay, or you are using suspendCancellableCoroutine to wrap a callback and resume/return the value as function return, and many more).
When the suspend function onePlusOne is called, it doesn't automatically suspend the coroutine that launch created right? If it does, how does it still execute the return 1+1 statement?
Yes it doesn't suspend it, unless you call a suspend function it is not suspended.
Suspend functions aren't magical that they suspend on their own, they are same as a non-suspend function, but they have one extra parameter appended at the time of compilation accepting a Continuation, which basically work as a callback to pause (suspend) and resume a coroutine.

Using Firebase with Kotlin coroutines: Task is not canceled when job is canceled

Inside a coroutine, with the help of await() function from "kotlinx-coroutines-play-services" library, i use something like this:
suspend fun uploadFile(uri: Uri) = withContext(IO) {
Firebase.storage.reference.child("example").putFile(uri).await()
}
The problem is when the Job of the current coroutine is canceled, this task is not canceled with it and keeps executing.
I need all nested tasks to auto-stop when the job is canceled. Is there a way I can achieve this?
If I understand you correctly you have your repository that is is application scoped, so that there is one instance of it in your app.
Let's assume you call your repository function from the ViewModel. You can use viewModelScope to call it from a coroutine that will be lifecycle aware and it will be stopped when the viewModel is destroyed.
It could look like this:
fun uploadFile(uri: Uri) = viewModelScope.launch(Dispatchers.IO) {
repo.uploadFile(uri)
}
And the repository function could now look like this:
suspend fun uploadFile(uri: Uri) {
Firebase.storage.reference.child("example").putFile(uri).await()
}
If you call it from the activity or the fragment not viewModel you can instead write:
lifecycleScope.launch(Dispatchers.IO){
repo.uploadFile(uri)
}
If you have nested calls like the repository is called by like a UseCase or sth else, you just need to add suspend keyword in every function on the way.
Edit:
You can cancel the coroutine, but unfortunately you cannot cancel the firebase request. So you want to handle the situation when you cancel the coroutine and the file should not be saved remotely. One simple way is to handle it in onDetach or sth else in fragment or activity. One trick you could use is to put you code in the repository in try block and add finally block. It will be run when the coroutine is canceled and there you could check whether file is saved and if so, delete it.
suspend fun uploadFile(uri: Uri) {
try {
Firebase.storage.reference.child("example").putFile(uri).await()
} finally {
// here handle canceled coroutine
}
}
You can read more about it here.

How to use coroutines GlobalScope on the main thread?

I'm trying to use the latest coroutines in 0.30.0, and having trouble figuring out how to use the new scoping. In the original coroutines I could set the context with UI or CommonPool and everything worked correctly.
Now I'm trying to use the GlobalScope in my ViewModel while reading from a room database, and then I want to assign the value returned to my LiveData object.
I'm getting the following error when I try to set the LiveData value
java.lang.IllegalStateException: Cannot invoke setValue on a
background thread
fun getContact() {
GlobalScope.launch {
val contact = contacts.getContact() // suspended function
withContext(Dispatchers.Default) { phoneContact.value = contact }
}
}
I only see Default, Unconfined and IO for dispatchers, and none of them work, I can't figure out what I'm doing wrong? Where is my option for the Main Thread?
You solved your immediate problem by adding the dependency, but let me add a note on your usage of GlobalScope.
Using the GlobalScope in production code is an antipattern. It's there for similar reasons like
runBlocking, to make it easy to do quick experiments. You should especially avoid it on Android due to the complicated lifecycle of app components.
If you're launching a coroutine from an Android event handler, you should use the current Activity as its coroutine scope. This will ensure your coroutine gets canceled when the activity gets destroyed. Without that the coroutine will go on, referring to the now-dead activity.
Here's a sample adapted from the documentation on CoroutineScope, it shows how to use your activity as the coroutine scope:
class MyActivity : AppCompatActivity(), CoroutineScope {
// Sets up the default dispatcher and the root job that we can use to centrally
// cancel all coroutines. We use SupervisorJob to avoid spreading the failure
// of one coroutine to all others.
override val coroutineContext: CoroutineContext =
Dispatchers.Main + SupervisorJob()
override fun onDestroy() {
super.onDestroy()
coroutineContext[Job]!!.cancel()
}
// this.launch picks up coroutineContext for its context:
fun loadDataFromUI() = this.launch {
// Switch to the IO dispatcher to perform blocking IO:
val ioData = withContext(Dispatchers.IO) {
// blocking I/O operations
}
draw(ioData) // use the data from IO to update UI in the main thread
}
}
If you're using a ViewModel, use it as the scope and cancel the master job from onClear.
If you're doing work from a background job, use your JobService implementation as the scope and use onStartJob and onStopJob the way we use onCreate and onDestroy above.
I was missing the Android portion of coroutines in my gradle file
implementation
"org.jetbrains.kotlinx:kotlinx-coroutines-android:0.30.0"
Once I had that, Dispatchers.Main appeared

Categories

Resources