Description
I ran into a weird issue that manifested itself when I updated the kotlinx-coroutines-core
dependency from 1.3.2 to 1.3.3. However, the self-contained example below reproduces the issue with 1.3.2 as well.
I have an extension method for a callback-based operation queue. This extension method uses suspendCancellableCoroutine
to wrap the callback usage and to convert it to a suspend function. Now, it all works otherwise, but the resulting object that is returned from the suspending function is not of type T
, but CompletedWithCancellation<T>
, which is a private class of the coroutine library.
The weird thing is, if I call c.resume("Foobar" as T, {})
inside the suspendCancellableCoroutine
, it works just fine. When using the callback routine, the value is a String
before passing to to c.resume()
, but it gets wrapped in a CompletedWithCancellation
object.
Here's the code that reproduces the issue:
@ExperimentalCoroutinesApi
class MainActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.plant(Timber.DebugTree())
setContentView(R.layout.activity_main)
val vm = ViewModelProviders.of(this)
.get(MainViewModel::class.java)
vm.liveData.observe(this, Observer {
findViewById<TextView>(R.id.mainText).text = "Got result: $it"
})
vm.getFoo()
}
}
@ExperimentalCoroutinesApi
class MainViewModel : ViewModel() {
private val manager = OperationManager()
val liveData = MutableLiveData<String>()
fun getFoo() {
viewModelScope.launch {
val op = Operation(manager, "Foobar")
val rawResult = op.get<Any>()
Timber.d("Raw result: $rawResult")
val op2 = Operation(manager, "Foobar")
val result = op2.get<String>()
Timber.d("Casted result: $result")
liveData.postValue(result)
}
}
}
class OperationManager {
private val operationQueue = ConcurrentLinkedQueue<Operation>()
private val handler = Handler(Looper.getMainLooper())
private val operationRunnable = Runnable { startOperations() }
private fun startOperations() {
val iter = operationQueue.iterator()
while (iter.hasNext()) {
val operation = iter.next()
operationQueue.remove(operation)
Timber.d("Executing operation $operation")
operation.onSuccess(operation.response)
}
}
fun run(operation: Operation) {
addToQueue(operation)
startDelayed()
}
private fun addToQueue(operation: Operation) {
operationQueue.add(operation)
}
private fun startDelayed() {
handler.removeCallbacks(operationRunnable)
handler.post(operationRunnable)
}
}
open class Operation(private val manager: OperationManager, val response: Any) {
private val listeners = mutableListOf<OperationListener>()
fun addListener(listener: OperationListener) {
listeners.add(listener)
}
fun execute() = manager.run(this)
fun onSuccess(data: Any) = listeners.forEach { it.onResult(data) }
}
@ExperimentalCoroutinesApi
suspend fun <T> Operation.get(): T = suspendCancellableCoroutine { c ->
@Suppress("UNCHECKED_CAST")
val callback = object : OperationListener {
override fun onResult(result: Any) {
Timber.d("get().onResult() -> $result")
c.resume(result as T, {})
}
}
addListener(callback)
execute()
}
interface OperationListener {
fun onResult(result: Any)
}
Do note that just before calling c.resume()
, the type of result
is String
, as it should be. However, it's not String
in getFoo()
once the suspend function completes. What causes this?