I am authoring a library “SDK” in Kotlin which wraps a c++ core. I would like to avoid launching Coroutines as much as possible, though I am not quite sure why this is important to me. I’d like to examine this feeling and get some perspective.
Clearly, I would spin up a thread if needed in Java as part of an analogous SDK, but in Kotlin it seems that I should design around asking the user to pass in a CoroutineScope, or even make extension functions on CoroutineScope, to make clear something will be launched in the background. These options do not seem appropriate for every situation. Extending CoroutineScope as part of our SDK seems risky from a namespace perspective. And with passing in a CoroutineScope, for example, to obtain a SharedFlow from Flow.callbackFlow using shareIn(CoroutineScope), I am hesitant to obtain the scope through some constructor parameter or mutable property, and definitely not through a method parameter as this would upset the callback cold flow. I am also aware of SharedFlow.tryEmit but I’d like to guarantee the delivery of all values to all collectors.
So I am wondering if I am misreading the principles of structured concurrency. Perhaps it is fine to use the default dispatcher to launch such work as calling emit on a SharedFlow in a library.
Nothing is forcing you to use coroutines. Or to launch them. An API made up entirely of suspend methods doesn’t necessarily need to worry about launching coroutines.
You can still do that. And if you want to use coroutines too, you can use the Executor.asCoroutineDispatcher() extension method. The question is, how does the app tell you to stop doing work on that thread? How does the app know when you are done doing background work. Structured concurrency is a builtin way to deal with those questions but it’s not necessary in every context.
I think that kinda depends on the nature of the background work happening.
Why? If you want to tie the lifetime of an object to a CoroutineScope, then that seems entirely appropriate.
I wouldn’t either
Not sure I follow. Obviously this works just fine for the shareIn operator.
You could also go with an unlimited buffer, then tryEmit will never fail. You essentially do this anyways if you always launch { emit(item) } since you are queueing coroutine continuations without limit. Either way you are saying you’d rather get an OutOfMemoryException than worry about scenarios where data is emitted faster than it’s handled.
If you describe the problem space a bit more, it’ll be easier for others to help you out. Right now, at least to me, your post reads like “What’s the best fit for my problem?” without ever quite describing the problem.
Some specific peices that I don’t know:
Does the class do work in the background while it’s active?
Is the launched work in response to specific calls?
If you were to add a CoroutineScope parameter to the constructor of your class, would it be used for anything besides shareIn arguments?
I second the idea of creating an API using suspending methods. I have really liked the feel of how Android core libraries have recently been augmenting a lot of their previous asynchronous APIs with suspending counterparts. A suspending method API allows consumers of your SDK to decide the lifecycle of asynchronous work for themselves by launching their own coroutines to call your API.
Sorry, I have a couple of different situations I am considering. There is no single problem, although the example of the SharedFlow is a situation I need to consider, among others. I am hesitant to describe the problems more for confidentiality reasons. But I think I have gleaned from your answer that I should consider each of my situations independently and at least in the case of the SharedFlow consider obtaining a CoroutineScope through a constructor property/parameter or use an “unlimited” buffer. Thank you.
I see no “unlimited” buffer option. Do you mean something like Int.MAX_VALUE for the extra buffer capacity? As far as preferring OOM, yes it is preferable.
More specifically, if you need the shareIn operator. If you are just stuffing values into a MutableSharedFlow, then you don’t need a CoroutineScope at all.