Launching coroutines vs suspend functions
Let's consider a use case, where you need to concurrently make a number of actions. There are two kinds of functions that can be used for that:
- a regular function operating on a coroutine scope object,
- a suspending function.
Those two options might look similar in some ways, but they represent essentially different use cases. To see some essential differences, you do not even need to read their bodies.
When we know that a function starts coroutines, and it is a regular function, we know it needs to use a scope object. Such functions typically only start coroutines and do not await their completion, so most likely
sendNotifications will take milliseconds to execute. Starting coroutines on external scope also means, that exceptions in those coroutines will be handled by this scope (most often printed and ignored). Those coroutines will inherit context from the scope object, and to cancel them, we need to cancel the scope.
When we know that a function starts coroutines, and it is a suspend function, we can assume that such a function will not finish until all the coroutines are finished. Such
sendNotifications will suspend until the last notification is fully handled. That is an important synchronization mechanism. We also know, that suspending functions do not cancel their parent. Instead, they might throw or silence exceptions, just like regular functions can throw or silence exceptions. The coroutines started by such a function should inherit context from and build relation with the function that called this function.
Both of those use cases are important in our applications. When we have a choice, we should prefer suspending functions. They are easier to control and synchronize. But coroutines need to start somewhere, and for that we use regular functions with a scope object.
In some applications, we might have a situation, where we need to mix those two kinds of functions. We start coroutine on suspend function scope, when this coroutine is part of the same process. Only then this coroutine inherits context, function awaits its completion, and is cancelled with this function. We launch a coroutine on an object representing scope when we want to start some independent process.
Now consider a function, that needs to execute some operations that are an essential parts of its execution, but it also needs to start an independent process. In the example below, the process of sending an event is considered an external process, so it is started on an external scope. Thanks to that:
updateUserwill not wait until
sendEventexecution is completed.
sendEventprocess will not be cancelled when the coroutine that has started
updateUseris cancelled. (What makes sense, user synchronization has completed anyway.)
eventsScopedecides what should be the context for sending events, and if they should happen or not. (If
eventsScopeis not active, event will not be sent)
In some situations, this hybrid behavior is what we want in our applications, but it should be used consciously.