Hi, I have an API-design related question. Let's s...
# coroutines
d
Hi, I have an API-design related question. Let's say I have some "Controller" object which has its own
CoroutineScope
(hidden from outside world). And some of its methods have the form like
fun startThis() = scope.launch { updateInternalState() }
, let's say it has 5 such methods. Now, do you think it is ok to also have some methods as
suspend
-methods? I.e the API would expose both launch-like and suspend-like methods and each of the latter could potentially be called from within another scope (while "Controller" has its own scope). To me this part feels a little bit weird, but maybe it's OK? I.e. I can always protect internal state access with
withContext
in suspend-methods, but still I'm not sure I feel good about such an API.
s
It makes sense to me. If you have a coroutine scope, you’re launching coroutines to do work. If you’re launching coroutines to do work, anything that wants the result from that work is going to need to
suspend
.
Maybe you can give some more information about what your object looks like, though?
k
You can return them as
Deferred
c
I think you’d be making a big mess for yourself if your “controller” object has a bunch of code running on different coroutineScopes. The Android ViewModel docs recommend not exposing suspend functions, and letting the ViewModel launch coroutines on its own. https://developer.android.com/kotlin/coroutines/coroutines-best-practices#viewmodel-coroutines
If you have a use-case where the consumer needs to control the coroutineScope, then it’s probably best to manage the data inside the controller with a StateFlow, and the consumer then subscribes to that Flow. This makes the subscription the responsibility of the consumer, but still gives the controller full control of the lifetime of that data
d
I have no concrete example it's just that during code review this thought kept appearing in various contexts. In general we have only "launch"-like or "suspend"-like methods, but sometimes it seems desirable to have "await"-like semantics for some of them, which suspend solves, but it sort-of pollutes "I launch coroutines in my own scope on my own terms" stance. Returning Deferred has crossed my mind, but the caller still would want to await on it and is it really different from having
suspend fun deferredOperation() = withContext(mine) { ... }
?
I think you’d be making a big mess for yourself if your “controller” object has a bunch of code running on different coroutineScopes.
Yep, that's what I usually think. But theoretically I still can protect sensitive access by using withContext + Mutex() if needed...
This makes the subscription the responsibility of the consumer
again, I agree and usually do it this way. (but see my comment above about "await-semantics")
c
Working with low-level coroutine primitives is usually not the best solution. There are higher coroutines mechanisms that are easier to use, less error-prone, and are generally easier to understand. It might require re-architecting your class, or re-thinking how the data is being managed/processed, but it’s usually worth it. Coroutines offer many better ways to manage async work than the typical thread-like synchronization primitives, allowing you to think at higher levels of abstraction that communicate the intent much more clearly. For example, this snippet shows a few different ways it could be done: https://pl.kotl.in/Kkl0mjKdI
I’ve found it really helpful to think more in terms of who owns and manages updates to data, rather than who’s controlling the work that’s running. It’s much easier to reason about a Controller class holding onto some value and being the source-of-truth for that value, rather than thinking of the consumers as holding onto the value but the Controller manages the async work that updates it.
d
Yes! Thank you for the detailed answer! It consoles me, because I usually design as you described in the state-flow example and usually urge others to do the same. But I had suspicions and decided to clear them 🙂 My usual rule is this: either the "entity" has its own scope and then it has no suspend functions in a public api or it has no scope and consists entirely of suspend functions to be used by other "scope" handling entities.
c
If you’re interested in a more out-of-the-box solution for this kind of workflow, I maintain the Ballast MVI library, which works just as well for the Repository/Controller layer of your app as it does for UI ViewModels
d
Hah, I guess this is the field where everyone has one. I do have one too, but I went a DSL route hiding those "when's" and also have another complimentary library which allows to combine this DSL with a "ui" layer in a nice reactive way, where ui intents are naturally plugged into `onEach`'s from this library. This other ui-part is not yet published though.