I have a basic architectural question. What or who...
# coroutines
a
I have a basic architectural question. What or who defines which coroutine to use? Example: • I have a notificationService which exposes a
SharedFlow<Notification>
NotificationService.notify(notification: Notifiy)
can be suspend or not. ◦ If
suspend
the caller should provide the scope ◦ If not
suspend
the
NotificationService
should have a scope in which to launch the
emit
The question is. What is better? In my opinion a Notification is a side-effect, so if a ViewModel requires to send a notification somewhere it isn't the responsibility of the ViewModel to make sure this succeeds. It is the responsibility of the
NotificationService
. The ViewModel's scope can get cancelled. Is this the right reasoning?
f
Without more content I would say yes, if the service must outlive the scope of the viewmodel, then the service should run un a different scope (likely the application one)? Why should NotificationService.notify suspending? Isn't it an instant (non-suspending) operation?
a
Well under the hood it is an emit to a Sharedflow, which is suspending. However, when handled from the service scope itself the function doesn’t need to be suspending
n
If you are calling
launch { emit(value) }
, then you have created an unordered infinite buffer system. The continuations all wait so no events are lost until you run out of memory. Calling
launch
means you lose ordering since two calls to
launch
can run the lambdas in any order (maybe that's a problem for you, maybe not). My impression is that
emit
is only really useful when it is running in the same coroutine as the work producing the emitted values so that suspending stops any new values from being produced since the producer coroutine has been suspended. I'd rather make that infinite buffer obvious with
MutableSharedFlow(extraBufferCapacity=Int.MAX_INT)
/`tryEmit` or
Channel(UNLIMITED)
/
trySend
and
channel.consumeAsFlow().shareIn(notificationScope)
. I'd think hard on if you really need EVERY notification to succeed or if you could go with a smaller buffer size and maybe implement a DROP_OLDEST strategy or something.
To answer the original question. Each CoroutineScope basically represents a lifetime. Launch coroutines based on the lifetime you want them to have. So, for example, if the notification work should outlive the view model, then it has a different lifetime and should use a different scope.
p
As said above, to me it’s mostly about lifecycle. Architecturally, I personally prefer to have everything as cold/declarative as possible, I would expose
suspend
in every domain object and the
ViewModel
(or whatever) makes the actual work I think this approach also facilitates unit testing
So basically, it’s
suspend
all the way beyond the ViewModel , and no suspend at all on View and ViewModel
On Android particularly, I find it a bit tricky to have your classes own their own
CoroutineScope
because that means you would need to manage its lifecycle manually (e.g call
scope.cancel()
or
cancelChildren()
eventually when the class is no longer in use and ready to be disposed. You generally want the class bound to some Android lifecycle (
LifecycleOwner
) , so you’d probably want to pass in the parent
CoroutineScope
context (so you inherit the job and cancellation properties), or some similar approach. I just find it simpler for custom Components to be cold, not own a scope, and expose suspend api
On the specific notification example where it is argued it’s not the ViewModel responsibility to make sure this succeeds, we’ll, some scope must own the computation. If ViewModel is not that scope but is the caller and wants to be notified if it succeeds or not only if it’s currently active, a possible pattern is:
Copy code
suspend fun notify() =
  ApplicationScope.async {
       actuallyNotifyAndReturnResult()
  }.join()
Launch computation coroutine elsewhere, wait result on current coroutine
a
@Patrick Steiger Although I understand your reasons in Android lifecycles, this is not entirely logical for me. Especially for something like a Notification system. We have 1 service which tracks the notifications and exposes a flow of them to a viewModel which renders them to screen. Other services might call this notification system and should just fire-and-forget. I don't want to have a service suspend when firing a notification. On the other hand, @Nick Allen makes a good point. There is not reason this should be suspending. tryEmit is also a good solution.