Mark
12/31/2019, 2:58 AMCoroutineScope
as an argument. In my case, I want to create a factory method to instantiate `MyClass`:
suspend fun createMyClass(scope: CoroutineScope): MyClass {
return MyClass(scope, createMyArg())
}
suspend fun createMyArg(): MyArg {...}
class MyClass(
private val scope: CoroutineScope,
private val myArg: MyArg
)
and I would use it from my AndroidViewModel
like:
viewModelScope.launch {
createMyClass(this).doSomething()
// do some more stuff
}
What is the proper way to write createMyClass()
? For example, how about this?
suspend fun createMyClass(): MyClass {
return MyClass(CoroutineScope(coroutineContext), createMyArg())
}
molikuner
12/31/2019, 3:29 AMscope
to instanceScope
to clarify the intended use. Otherwise one could misunderstand that this was the scope that the function would be executed in. This indeed wouldn’t make any sense because of the suspend modifier.
Apart from all that I would make the scope a receiver to somehow follow the convention. A function, that launches it’s own coroutines (this time the class probably launches them) and returns earlier, than those coroutines end, is an extension function on a `CoroutineScope`:
suspend fun CoroutineScope.createMyClass(): MyClass {
return MyClass(this, createMyArg())
}
Adam Powell
12/31/2019, 3:32 AMCoroutineScope
implies all sorts of things about what that object might do to it and what valid states of that object areAdam Powell
12/31/2019, 3:33 AMviewModelScope.launch {
val myObj = createMyClass(this)
myObj.doSomething()
someOtherObject.obj = myObj
}
Adam Powell
12/31/2019, 3:34 AMCoroutineScope
given to it can be completed or cancelled even though a reference to the object escaped to somewhere else in your system. How does a MyClass
instance behave when this happens?Mark
12/31/2019, 3:35 AMMyClass
is wrapping a Channel
and declares functions that launch coroutines (using passed-in scope) to send to that channel.Adam Powell
12/31/2019, 3:35 AMMyClass
only taking a scope for the sake of injecting context elements from elsewhere?Adam Powell
12/31/2019, 3:36 AMMyClass
themselves be declared suspend
instead of giving a CoroutineScope
to MyClass
?Mark
12/31/2019, 3:37 AMAdam Powell
12/31/2019, 3:39 AMMark
12/31/2019, 3:41 AMMark
12/31/2019, 3:42 AMAudioTrack
Adam Powell
12/31/2019, 3:43 AMMark
12/31/2019, 3:45 AMAdam Powell
12/31/2019, 3:46 AMMark
12/31/2019, 3:48 AMAdam Powell
12/31/2019, 3:49 AMMyClass
Mark
12/31/2019, 3:52 AMvar myClass: MyClass? = null
fun onSomething() {
viewModelScope.launch {
if (myClass == null)
myClass = createMyClass(this)
}
myClass.doSomething()
}
}
although this code doesn’t fill me with confidence!Adam Powell
12/31/2019, 3:53 AMAdam Powell
12/31/2019, 3:53 AMAdam Powell
12/31/2019, 3:54 AMAdam Powell
12/31/2019, 3:55 AMAdam Powell
12/31/2019, 3:55 AMAdam Powell
12/31/2019, 3:56 AMMark
12/31/2019, 3:56 AMAdam Powell
12/31/2019, 3:56 AMsuspend fun
you expect that it does all of its work and either returns or throws; that it doesn't leak launched coroutines into other scopes as a side effectAdam Powell
12/31/2019, 3:57 AMcreateMyClass()
from the last snippet in the OP violates that expectationAdam Powell
12/31/2019, 3:57 AMAdam Powell
12/31/2019, 3:58 AMAdam Powell
12/31/2019, 4:00 AMcoroutineScope {}
or withContext(...) {
do - they suspend until all work from the body lambda block is doneMark
12/31/2019, 4:02 AMAdam Powell
12/31/2019, 4:02 AMAdam Powell
12/31/2019, 4:03 AMCoroutineScope
, and it's expected that the external owner of that scope can and will cancel it laterMark
12/31/2019, 4:03 AMAdam Powell
12/31/2019, 4:04 AMoffer
to send messages to that channel isn't really feasible here to cut this dependency? An unlimited-buffer channel vs. launching a bunch of sends isn't going to behave that much differently in practice here but it might help clean this up a bitMark
12/31/2019, 4:04 AMMark
12/31/2019, 4:04 AMAdam Powell
12/31/2019, 4:05 AMMark
12/31/2019, 4:07 AMAdam Powell
12/31/2019, 4:09 AMMark
12/31/2019, 4:10 AMMark
12/31/2019, 4:14 AMcreateMyClass()
from the last snippet” doesn’t actually launch other coroutines, but rather returns a class that does that when invoked to do so. Isn’t that a key difference?Adam Powell
12/31/2019, 4:14 AMcreateMyClass
doesn't need to suspend itself, it shouldn't be marked suspend
Mark
12/31/2019, 4:15 AMAdam Powell
12/31/2019, 4:17 AMAdam Powell
12/31/2019, 4:18 AMcreateMyClass
returnsAdam Powell
12/31/2019, 4:19 AMMyClass
then very much does 🙂Mark
12/31/2019, 4:20 AMMyClass
!Mark
12/31/2019, 4:21 AMAdam Powell
12/31/2019, 4:21 AMMark
12/31/2019, 4:21 AMAdam Powell
12/31/2019, 4:22 AMcreateMyArg
Mark
12/31/2019, 4:22 AMDeferred<MyClass>
is a better wayAdam Powell
12/31/2019, 4:23 AMMark
12/31/2019, 4:26 AMAdam Powell
12/31/2019, 4:28 AMviewModelScope.launch {
val myObj = MyClass(createMyArg())
launch { myObj.run() }
myObj.doStuff()
}
where run
will receive the inbound messages from the channel and handle them; essentially being the actorAdam Powell
12/31/2019, 4:29 AMMyClass
or lift it out; it's the straddling that makes things awkwardAdam Powell
12/31/2019, 4:30 AMviewModelScope
, pushing more of it into MyClass
I think is going to be more difficult than the alternativeAdam Powell
12/31/2019, 4:33 AMlaunch {
try {
myObj.run()
} catch (e: MyException) {
// recover, log, retry...
}
}
Adam Powell
12/31/2019, 4:34 AMviewModelScope
while still getting the error handling benefits of coroutinesMark
12/31/2019, 4:35 AMrun()
method (in the case where you don’t want MyClass holding a scope reference).Adam Powell
12/31/2019, 4:37 AM.run()
doesn't have to return until the actor completes. Or "actor" if you eliminate the extra launch and just have run
loop on channel receive and be the actor itself rather than using the actual actor {}
api that launchesMark
12/31/2019, 4:38 AMAdam Powell
12/31/2019, 4:42 AMchannel.consumeEach {}
or .consumeAsFlow()...collect {}
in there and you get the channel closing when the whole thing tears down like you described too, without having to register or do it elsewhereMark
12/31/2019, 4:52 AMcreateMyArg()
is called within MyClass
because it tidies things up significantly.
suspend fun createMyArg(): MyArg
class MyClass(private val scope: CoroutineScope) {
private lateinit var myArg: MyArg
private val sendChannel: SendChannel<Msg> = scope.actor {
myArg = createMyArg()
for (msg in channel) { // iterate over incoming messages
...
}
}
}
var myClass: MyClass? = null
fun onSomething() {
if (myClass == null)
myClass = createMyClass(viewModelScope)
}
myClass.doSomething()
}
fun createMyClass(scope: CoroutineScope): MyClass {
return MyClass(scope)
}
Adam Powell
12/31/2019, 4:53 AMmyArg
even have to be a property in that version or is it only used within the actor?Mark
12/31/2019, 4:55 AMAdam Powell
12/31/2019, 4:55 AMscope
for that matter if it's only used to launch the actor on construction and you buffer it so that you can offer
instead of `launch`/`send`?Adam Powell
12/31/2019, 4:56 AM.use {}
around the body of the actor loop instead?Mark
12/31/2019, 5:01 AMsuspend fun createMyArg(): MyArg
class MyClass(private val scope: CoroutineScope) {
private val sendChannel: SendChannel<Msg> = scope.actor {
val myArg = createMyArg()
try {
for (msg in channel) { // iterate over incoming messages
...
}
} finally {
myArg.tearDown()
}
}
}
var myClass by lazy { MyClass(viewModelScope) }
fun onSomething() {
myClass.doSomething()
}
Mark
12/31/2019, 5:01 AMAdam Powell
12/31/2019, 5:02 AMMark
12/31/2019, 5:02 AMAdam Powell
12/31/2019, 5:04 AMoffer
instead to do that thoughMark
12/31/2019, 5:18 AMAdam Powell
12/31/2019, 5:22 AMAdam Powell
12/31/2019, 5:23 AMMark
12/31/2019, 6:16 AMAdam Powell
12/31/2019, 6:19 AMMark
12/31/2019, 6:23 AMviewModelScope
and so if viewModelScope
was cancelled, then everything would be more consistent. With an API like CoroutineScope.playAsync()
- there would potentially be calls like GlobalScope.playAsync()
Adam Powell
12/31/2019, 6:30 AMMark
12/31/2019, 6:31 AMfun playAsync(): Job
?Adam Powell
12/31/2019, 6:32 AMsuspend fun
over returning Deferred
or Job
and performing a launch under the hood, it's so much easier to handle everythingMark
12/31/2019, 6:33 AMAdam Powell
12/31/2019, 6:33 AMAdam Powell
12/31/2019, 6:35 AMGlobalScope.launch {}
and treat it that way if they really think that's the case they haveAdam Powell
12/31/2019, 6:35 AMoffer
to a channel 🙂Adam Powell
12/31/2019, 6:37 AMoffer
is coming from and the associated desire to involve more coroutine launches as an alternative, especially when there's already a channel in play that guarantees serialization, etc.Adam Powell
12/31/2019, 6:38 AMsuspend
and call send
if you want to have a small/no buffer and let the caller decide to cancel because it doesn't care anymore, or use offer
if you want it to be fire and forget and you don't want to (or can't) suspend for a send and set the buffer policy accordinglyMark
12/31/2019, 6:43 AMAdam Powell
12/31/2019, 6:45 AMAdam Powell
12/31/2019, 6:46 AMMark
12/31/2019, 6:46 AMAudioTrack
?Adam Powell
12/31/2019, 6:47 AMMark
12/31/2019, 6:48 AMMark
12/31/2019, 6:50 AM