You don't necessarily need to use coroutine machinery to achieve what you want here. How would you phrase and approach this question if coroutines weren't involved? You could still have exactly the same scenario, where you want to join an existing listener if there is one, or add a new listener otherwise.
The solutions involving coroutine machinery are likely to be a bit messy. In most cases you're going to need a separate coroutine scope, as you'll be launching coroutines that are shared between multiple consumers.
I think there's a lot of value in solving this problem for the general case of callbacks with no coroutines. Then you can simply apply that solution to your existing code.
My solution involves creating a new listener which will wrap all the existing listeners. It will register itself as a listener with the third party library. When it receives the callback, it will fan-out and notify all of its own listeners.
I'm using a lock-free Trieber stack to store the list of listeners. Not only does this make the whole thing thread-safe without locking, but it also means we can atomically identify when the list goes from empty to non-empty. That makes it easy to know when to invoke the third-party library. If you think this is overkill, it would be easy enough to make a simpler implementation that uses a lock.
class SingleListenerObservable(private val delegate: Observable) : Observable {
private class ListenerStackNode(val listener: Listener, val next: ListenerStackNode?)
private val listeners = AtomicReference<ListenerStackNode?>()
private val actualListener = object : Listener {
override fun theThingHappened() {
val head = clear()
generateSequence(head) { it.next }
.forEach { it.listener.theThingHappened() }
}
}
override fun addListener(listener: Listener) {
val head = push(listener)
if (head.next == null) {
// we are the first listener
delegate.addListener(actualListener)
}
}
/**
* Add a listener to the stack, and return the new head of the stack
*/
private fun push(listener: Listener): ListenerStackNode {
var newHead: ListenerStackNode
do {
val oldHead = listeners.get()
newHead = ListenerStackNode(listener, oldHead)
val success = listeners.compareAndSet(oldHead, newHead)
} while (!success)
return newHead
}
/**
* Clear the stack, and return the old head of the stack
*/
private fun clear(): ListenerStackNode? {
var oldHead: ListenerStackNode?
do {
oldHead = listeners.get()
val success = listeners.compareAndSet(oldHead, null)
} while (!success)
return oldHead
}
}
Using this approach, you could just wrap your third party library with this decorator class, and use suspendCancellableCoroutine
to add listeners as you normally would.