[我正试图绕开suspendCoroutine
和suspendCancellableCoroutine
。我认为它们在以下情况下可能很有用:
这会编译,但绝不会使其超出“延迟时间”,即,延续不会恢复:
import kotlinx.coroutines.*
fun main(args: Array<String>) {
println("Hello, world!")
runBlocking {
launch {
postComment()
}
}
}
var isLoggedIn = false
var loginContinuation: CancellableContinuation<Unit>? = null
suspend fun postComment() {
if (!isLoggedIn) {
showLoginForm()
suspendCancellableCoroutine<Unit> {
loginContinuation = it
}
}
// call the api or whatever
delay(1000)
println("comment posted!")
}
suspend fun showLoginForm() {
println("show login form")
// simulate delay while user enters credentials
delay(1000)
println("delay over")
isLoggedIn = true
// resume coroutine on submit
loginContinuation?.resume(Unit) { println("login cancelled") }
}
我已经尝试了所有我能想到的一切,包括将调用移至登录检查之外的suspendCancellableCoroutine
,将showLoginForm
的内容包装在withContext(Dispatchers.IO)
中,使用coroutineScope.launch(newSingleThreadContext("MyOwnThread")
等。我从中得到的印象阅读互联网是这是一个有效的用例。我在做什么错?
首先,您误解了suspend
功能的概念。调用函数showLoginForm()
不会not启动新的协程。单个协程中的代码始终按顺序执行-首先,您调用showLoginForm()
,它会延迟,因为loginContinuation
为null
,所以它不会继续任何继续,然后suspendCancellableCoroutine
会永远挂起协程并导致死锁。
启动执行showLoginForm()
的新协程可以使您的代码正常工作:
suspend fun CoroutineScope.postComment() {
if (!isLoggedIn) {
launch {
showLoginForm()
}
suspendCancellableCoroutine<Unit> {
loginContinuation = it
}
}
// call the api or whatever
delay(1000)
println("comment posted!")
}
此代码仍然会失败(*),但在这种情况下不会失败。此代码的工作版本如下所示:
import kotlin.coroutines.*
import kotlinx.coroutines.*
fun main(args: Array<String>) {
println("Hello, world!")
runBlocking {
postComment()
}
}
var isLoggedIn = false
suspend fun CoroutineScope.postComment() {
if (!isLoggedIn) {
suspendCancellableCoroutine<Unit> { continuation ->
launch {
showLoginForm(continuation)
}
}
}
delay(1000)
println("comment posted!")
}
suspend fun showLoginForm(continuation: CancellableContinuation<Unit>) {
println("show login form")
delay(1000)
println("delay over")
isLoggedIn = true
continuation.resume(Unit) { println("login cancelled") }
}
而且,在您的示例中,不需要暂停协程。如果我们只需在同一协程中执行其代码,为什么还需要另一个协程?我们需要等到它完成为止。由于协程按顺序执行代码,因此只有在if
完成后,我们才会转到showLoginForm()
分支之后的代码:
var isLoggedIn = false
suspend fun postComment() {
if (!isLoggedIn) {
showLoginForm()
}
delay(1000)
println("comment posted!")
}
suspend fun showLoginForm() {
println("show login form")
delay(1000)
println("delay over")
isLoggedIn = true
}
此方法最适合您的示例,因为所有代码都是顺序的。
(*)-如果在suspendCancellableCoroutine
完成后调用showLoginForm
,则此代码仍可能导致死锁-例如,如果删除delay
中的showLoginForm
调用或使用多线程调度程序-在JVM中不保证suspendCancellableCoroutine
的调用将早于showLoginForm
。此外,loginContinuation
不是@Volatile
,因此对于多线程调度程序,代码也可能由于可见性问题而失败-执行showLoginForm
的线程可能会观察到loginContinuation
为null
。
绕过Continuations很混乱,很容易导致您遇到错误...在Continuation甚至被分配给continuation属性之前,一个函数就完成了。
由于登录表单就是您想要转换为暂停功能的地方,因此应在此使用suspendCoroutine
。 suspendCoroutine
是一种低级代码,应将其放置得尽可能低,以便您的主程序逻辑可以使用易于读取的顺序协程,而无需嵌套的launch
/ suspendCoroutine
调用。
var isLoggedIn = false
suspend fun postComment() {
if (!isLoggedIn) {
showLoginForm()
}
println("is logged in: $isLoggedIn")
if (isLoggedIn) {
// call the api or whatever
delay(1000)
println("comment posted!")
}
}
suspend fun showLoginForm(): Unit = suspendCancellableCoroutine { cont ->
println("Login or leave blank to cancel:")
//Simulate user login or cancel with console input
val userInput = readLine()
isLoggedIn = !userInput.isNullOrBlank()
cont.resume(Unit)
}
我没有在delay()
中使用showLoginForm()
,因为您无法在suspendCancellableCoroutine
块中调用挂起函数。最后三行也可以包装在scope.launch
中,并使用delay
而不是readLine
,但实际上,您的UI交互并不是延迟的协程。
编辑:
试图将延续性传递给另一个活动会特别混乱。 Google甚至不建议在一个应用程序中使用多个活动,因为很难在它们之间传递对象。要使用Fragments进行操作,您可以编写LoginFragment类以具有如下这样的私有延续属性:
class LoginFragment(): Fragment {
private val continuation: Continuation<Boolean>? = null
private var loginComplete = false
suspend fun show(manager: FragmentManager, @IdRes containerViewId: Int, tag: String? = null): Boolean = suspendCancelableCoroutine { cont ->
continuation = cont
manager.beginTransaction().apply {
replace(containerViewId, this@LoginFragment, tag)
addToBackStack(null)
commit()
}
}
// Call this when login is complete:
private fun onLoginSuccessful() {
loginComplete = true
activity?.fragmentManager?.popBackStack()
}
override fun onDestroy() {
super.onDestroy()
continuation?.resume(loginComplete)
}
}
然后您将显示来自另一个片段的片段,如下所示:
lifecycleScope.launch {
val loggedIn = LoginFragment().show(requireActivity().fragmentManager, R.id.fragContainer, null)
// respond to login state here
}
只要您使用片段的lifecycleScope
而不是活动的lifecycleScope
,我认为您应该免受屏幕旋转的影响。但是我自己还没有这样做。