首页 > 解决方案 > Kotlin Coroutine 抛出 JobCancellationException 并触发观察者

问题描述

我正在架构中实现kotlin coroutinesMVVM替换我的RxJava依赖项。

我的问题:注销后(这个细节很重要,因为我在重新开始时没有异常),我JobCancellationException在启动时抛出了一个协程异常,并且我在 LoginActivity 中的所有内容都被触发了(wtf?)viewModelScope 甚至observers如果该try/catch块未执行(断点),那么它完全破坏了我的代码逻辑。

我的层如下ActivitysetOnClickListener触发,调用viewModel函数-> ,在内部ViewModel创建和调用->,从调用和调用in- >调用ApicoroutineviewModelScopesuspend function Repositorysuspended functionviewModelsuspend functionApiService suspend function

用例:我登录新启动的应用程序(LoginActivity)。我的堆栈被清除以启动MainActivity. 我去SettingsActivity注销。我再次注销并清除堆栈以开始一个全新的LoginActivity. 当我再次尝试登录时,我被exception抛出并被observers触发。

登录活动:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        tvLogin.setSafeOnClickListener { login() }
}
private fun login() {
        Firebase.analytics.logEvent("hit_login", null)
        runOnUiThread {
            pDialog = SweetAlertDialog(this, SweetAlertDialog.PROGRESS_TYPE)
            pDialog!!.progressHelper.barColor = Color.parseColor("#2A6022")
            pDialog!!.titleText =
                "<font color='#2A6022'>" + resources.getString(R.string.signing) + "</font>"
            pDialog!!.setCancelable(false)
        }
        var pwd = etPassword.text.toString()
        if (passedBiometric) {
            pwd = sessionManager.decrypt(sessionManager.loadUsername())
        }
        passedBiometric = false
        viewModel.login(etUsername.text.toString(), pwd)
        viewModel.error.observe(this, {
            Toast.makeText(this, getString(it), Toast.LENGTH_SHORT).show()
        })
        viewModel.loadingState.observe(this, {
        Timber.e("LoadingState Observer triggered")
            when (it) {
                LoadingState.LOADED -> {
                    pDialog!!.dismiss()
                    Timber.e("LOADED")
                    startMainActivity()
                }
                LoadingState.FAILED -> {
                    pDialog!!.dismiss()
                    Timber.e("FAILED")
                    viewModel.error.value?.let { errorRsc ->
                        Toast.makeText(this, getString(errorRsc), Toast.LENGTH_SHORT).show()
                    }
                }
                LoadingState.LOADING -> {
                    pDialog!!.show()
                    Timber.e("LOADING")
                }
            }
        })
  }

private fun startMainActivity() {
    val intent = Intent(this, MainActivity::class.java)
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
    intent.action = this.intent.action
    intent.putExtra(ProfileFragment.USER, viewModel.user.value)
    startActivity(intent)
    finish()
}

登录视图模型:

class LoginViewModel(private val repo: ApiRepository, private val sessionManager: SessionManager) :
    ViewModel() {
    private val _error = MutableLiveData<Int>()
    val error: LiveData<Int>
        get() = _error
    private val _loadingState = MutableLiveData<LoadingState>()
    val loadingState: LiveData<LoadingState>
        get() = _loadingState
    private val token = MutableLiveData<SessionToken>()
    private val _user = MutableLiveData<User>()
    val user: LiveData<User>
        get() = _user

fun login(username: String, password: String) {
    try {
        Timber.e("enter try block outside viewModelScope.launch")
        viewModelScope.launch {
            try {
                Timber.e("try block inside viewModelScope.launch")
                _loadingState.value = LoadingState.LOADING
                token.value = repo.login(username, password)
                sessionManager.storeSessionToken(token.value!!.token)
                sessionManager.storeUsername(username)
                sessionManager.encrypt(username, password)
                _user.value = repo.getUser()
                _loadingState.value = LoadingState.LOADED
            } catch (e: Exception) {
                Timber.e("catch block inside viewModelScope.launch")
                if (e is HttpException) {
                    when (e.code()) {
                        500, 503 -> {
                            _error.value = R.string.error_api_down
                        }
                        400 -> {
                            _error.value = R.string.error_bad_credentials
                        }
                    }
                }
                _loadingState.value = LoadingState.FAILED
            }
        }
    }
    catch (e: Exception){
        Timber.e("Catch block outside viewmodelScope.launch")
        when (e){
            is CancellationException -> { Timber.e("CancellationException caught")}
            else -> { Timber.e("Unknown caught")}
        }
    }
}

}

Api 存储库:

class ApiRepository(
    private val apiService: ApiService,
    private val sessionManager: SessionManager
) {
    suspend fun login(username: String, password: String): SessionToken {
        val map = mutableMapOf<String, String>()
        map["username"] = username
        map["password"] = password
        return apiService.login(
            JSONObject(map.toMap()).toString()
                .toRequestBody("application/json".toMediaTypeOrNull())
        )
    }

    suspend fun getUser(): User {
        return apiService.getUser(sessionManager.loadSessionToken())
    }

}

api服务:

interface ApiService {
    companion object {
        private const val AUTH = "Authorization"
    }
    @POST(ApiRouting.URL_TOKEN)
    suspend fun login(@Body requestBody: RequestBody): SessionToken
    @GET(ApiRouting.URL_GET_USER)
    suspend fun getUser(@Header(AUTH) token: String): User

}

设置活动:

private fun initListeners() {
    tvLogout.setSafeOnClickListener {
        DashboardManager.clearValues()
        sessionManager.clearSessionToken()
        val intent = Intent(this, LoginActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        startActivity(intent)
        finish()
    }
}

TLDR:为什么抛出异常?为什么会触发观察者?为什么这种情况只发生在注销后而不是重新开始?

我尝试在启动中添加 coroutineScope 并在存储库中添加另一个范围,withContext(Dispatchers.IO)但它没有改变任何东西

我是一个非常初学者,kotlin coroutines所以如果你有任何建议,链接或任何可能有助于欢迎的东西。或者,如果您需要任何其他代码块,请告诉我。

编辑:当我进入包围我的 try catch 块LoginViewModel和我的开始时(我在.MainActivityviewModelScope.launchMainActivityobserverLoginAvtivity

为了让您更清楚地了解新的(和成功的)开始是什么样的:

E/LoginViewModel: enter try block outside viewModelScope.launch
E/LoginViewModel$login: try block inside viewModelScope.launch
E/LoginActivity$login: LoadingState Observer triggered
V/ViewRootImpl: The specified message queue synchronization  barrier token has not been posted or has already been removed
E/LoginActivity$login: LOADING
    LoadingState Observer triggered
    LOADING
I/okhttp.OkHttpClient: --> POST http://45.153.184.141/api-token-auth/
    Content-Type: application/json; charset=utf-8
    Content-Length: 46
    --> END POST (46-byte body)
E/LoginActivity$login: LoadingState Observer triggered
    LOADING
D/DecorView: onWindowFocusChangedFromViewRoot hasFocus: true, DecorView@7175d32[LoginActivity]
W/Choreographer: Already have a pending vsync event.  There should only be one at a time.
V/ViewRootImpl: The specified message queue synchronization  barrier token has not been posted or has already been removed
W/Choreographer: Already have a pending vsync event.  There should only be one at a time.
I/okhttp.OkHttpClient: <-- 200 OK http://xx.xx.xx.xx/api-token-auth/ (237ms)
    Date: Wed, 03 Mar 2021 21:31:39 GMT
    Server: WSGIServer/0.2 CPython/3.8.2
    Content-Type: application/json
    Allow: POST, OPTIONS
    X-Frame-Options: DENY
    Content-Length: 52
    Vary: Cookie
    X-Content-Type-Options: nosniff
    <-- END HTTP (52-byte body)
W/r.myweedmanage: Accessing hidden method Lsun/misc/Unsafe;->getObject(Ljava/lang/Object;J)Ljava/lang/Object; (greylist, linking, allowed)
    Accessing hidden method Lsun/misc/Unsafe;->putObject(Ljava/lang/Object;JLjava/lang/Object;)V (greylist, linking, allowed)
W/r.myweedmanage: Accessing hidden method Lsun/misc/Unsafe;->getObject(Ljava/lang/Object;J)Ljava/lang/Object; (greylist, linking, allowed)
    Accessing hidden method Lsun/misc/Unsafe;->putInt(Ljava/lang/Object;JI)V (greylist, linking, allowed)
W/r.myweedmanage: Accessing hidden method Lsun/misc/Unsafe;->getLong(Ljava/lang/Object;J)J (greylist,core-platform-api, linking, allowed)
    Accessing hidden method Lsun/misc/Unsafe;->getLong(Ljava/lang/Object;J)J (greylist,core-platform-api, linking, allowed)
D/TransportRuntime.SQLiteEventStore: Storing event with priority=DEFAULT, name=FIREPERF for destination cct
D/TransportRuntime.JobInfoScheduler: Scheduling upload for context TransportContext(cct, DEFAULT, MSRodHRwczovL2ZpcmViYXNlbG9nZ2luZy1wYS5nb29nbGVhcGlzLmNvbS92MS9maXJlbG9nL2xlZ2FjeS9iYXRjaGxvZ1xBSXphU3lDY2traUg4aTJaQVJ3T3MxTEV6RktsZDE1YU9HOG96S28=) with jobId=877081683 in 30000ms(Backend next call timestamp 1614807010307). Attempt 1
I/okhttp.OkHttpClient: --> GET http://45.153.184.141/api/users/profile/
    Authorization: Token token
    --> END GET
D/OnePlusJankManager:  Chor uploadMDM JANK_TYPE_ONCE mViewTitle = com.weedmanager.myweedmanager/com.weedmanager.myweedmanager.authentication.LoginActivity--- jank level = 1
I/okhttp.OkHttpClient: <-- 200 OK http://45.153.184.141/api/users/profile/ (83ms)
    Date: Wed, 03 Mar 2021 21:31:40 GMT
    Server: WSGIServer/0.2 CPython/3.8.2
    Content-Type: application/json
    Vary: Accept
    Allow: GET, HEAD, OPTIONS
    X-Frame-Options: DENY
    Content-Length: 103
    X-Content-Type-Options: nosniff
    <-- END HTTP (103-byte body)
W/r.myweedmanage: Accessing hidden method Lsun/misc/Unsafe;->putObject(Ljava/lang/Object;JLjava/lang/Object;)V (greylist, linking, allowed)
W/r.myweedmanage: Accessing hidden method Lsun/misc/Unsafe;->getInt(Ljava/lang/Object;J)I (greylist, linking, allowed)
    Accessing hidden method Lsun/misc/Unsafe;->getObject(Ljava/lang/Object;J)Ljava/lang/Object; (greylist, linking, allowed)
E/LoginActivity$login: LoadingState Observer triggered
W/r.myweedmanage: Accessing hidden method Lsun/misc/Unsafe;->getLong(Ljava/lang/Object;J)J (greylist,core-platform-api, linking, allowed)
    Accessing hidden method Lsun/misc/Unsafe;->getLong(Ljava/lang/Object;J)J (greylist,core-platform-api, linking, allowed)
E/LoginActivity$login: LOADED
D/TransportRuntime.SQLiteEventStore: Storing event with priority=DEFAULT, name=FIREPERF for destination cct
D/DecorView: onWindowFocusChangedFromViewRoot hasFocus: true, DecorView@4a8ec73[LoginActivity]
D/TransportRuntime.JobInfoScheduler: Upload for context TransportContext(cct, DEFAULT, MSRodHRwczovL2ZpcmViYXNlbG9nZ2luZy1wYS5nb29nbGVhcGlzLmNvbS92MS9maXJlbG9nL2xlZ2FjeS9iYXRjaGxvZ1xBSXphU3lDY2traUg4aTJaQVJ3T3MxTEV6RktsZDE1YU9HOG96S28=) is already scheduled. Returning...
E/LoginActivity$login: LoadingState Observer triggered
    LOADED
E/LoginActivity$login: LoadingState Observer triggered
    LOADED
D/OnePlusJankManager:  Chor uploadMDM JANK_TYPE_ONCE mViewTitle = com.weedmanager.myweedmanager/com.weedmanager.myweedmanager.authentication.LoginActivity--- jank level = 1
V/FA: Recording user engagement, ms: 1494
W/ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@c6ecc8e
V/FA: Activity paused, time: 1057027342
D/FA: Event not sent since app measurement is disabled
V/FA: onActivityCreated
E/MainActivity: MainActivity onCreate

现在在魔法发生后注销:

E/LoginViewModel: enter try block outside viewModelScope.launch
D/FA: Event not sent since app measurement is disabled
E/LoginActivity$login: LoadingState Observer triggered
E/LoginActivity$login: LOADED
V/ViewRootImpl: The specified message queue synchronization  barrier token has not been posted or has already been removed
V/FA: Recording user engagement, ms: 8496
V/FA: Connecting to remote service
V/FA: Activity paused, time: 1057275561
W/Choreographer: Already have a pending vsync event.  There should only be one at a time.
D/FA: Event not sent since app measurement is disabled
W/ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@24faad3
E/MainActivity: MainActivity onCreate

所以我们可以清楚地看到它没有进入到try里面的块中,viewModelScope.launch因为没有显示日志并且没有发出请求(profiler也是空的)但是没有Exceptiontry/catch包围在块中viewModelScope.launch

事实上,当我使用和JobCancellationException调试我的代码时,我发现了。breakpointstep over

viewModelScope.launch我试图通过从更改为更改我的范围CoroutineScope(Dispatchers.IO).launch(只是根据问题下方的评论使用此范围进行测试)并且它崩溃了。

标签: androidkotlinmvvmkotlin-coroutinesandroid-viewmodel

解决方案


我终于解决了我的问题。确实,我正在使用Koinfor dependency injection。我只是简单地替换了单:

val viewModelModule = module {
    single { StatsViewModel(get()) }
    single{ LoginViewModel(get(), get()) }
}

按视图模型:

val viewModelModule = module {
    viewModel { StatsViewModel(get()) }
    viewModel { LoginViewModel(get(), get()) }
}

事实上,single 是一个单例,因此通过使用viewModel关键字,theviewModel绑定到 Android 组件生命周期。

https://insert-koin.io/docs/reference/koin-android/viewmodel/


推荐阅读