为什么在访问DataStore的 Flow时会一直阻塞?
我正在开发一个应用,其主要功能是在用户按下按钮时与网络服务器通信,并在应用可能被关闭时在后台间歇性地继续通信。现在我在尝试找出一种在应用关闭和重新打开之间能够持久保存cookie数据的方法。为此我尝试使用偏好设置数据存储(遵循Android的基础用法),但当我尝试收集Flow以获取可直接放入API请求的字符串时,该函数会无限期阻塞。
class CookieRepository(private val dataStore: DataStore<Preferences>) {
private companion object {
val CSRF = stringPreferencesKey("csrf")
val SESSION = stringPreferencesKey("session")
}
val TAG = "COOKIE_REPOSITORY"
suspend fun saveCsrf(csrfToken: String) {
dataStore.edit { preferences -> preferences[CSRF] = csrfToken }
}
suspend fun saveSession(sessionToken: String) {
dataStore.edit { preferences -> preferences[SESSION] = sessionToken }
}
val csrfToken: Flow<String> = dataStore.data
.catch {
if (it is IOException) {
Log.e(TAG, "Error reading preferences.", it)
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences -> preferences[CSRF] ?: "No Cookie" }
val sessionToken: Flow<String> = dataStore.data
.catch {
if (it is IOException) {
Log.e(TAG, "Error reading preferences.", it)
emit(emptyPreferences())
} else {
throw it
}
}
.map { preferences -> preferences[SESSION] ?: "No Cookie" }
suspend fun updateCookiesFromHeaders(headers: Headers) {
for (header in headers) {
if (header.first == "Set-Cookie") {
val name = header.second.split("=")[0]
val value = header.second.split("=")[1].split(";")[0]
if (name == "csrftoken") {
saveCsrf(value)
} else if (name == "sessionid") {
saveSession(value)
}
}
}
}
suspend fun getCookiesAsString(): String {
var cookieString = ""
csrfToken.collect { if (it != "No Cookie") {cookieString = cookieString.plus("csrftoken=$it;")} }
sessionToken.collect { if (it != "No Cookie") {cookieString = cookieString.plus("sessionid=$it;")} }
return cookieString.dropLast(1)
}
suspend fun getCsrfToken(): String {
var token = ""
csrfToken.collect { token = it }
return token
}
}
The CookieRepository is created in my Application.kt injected into my view model, which calls the functions updateCookiesFromHeaders, getCookiesAsString and getCsrfToken before making the request.
解决方案
阻塞行为来自 collect 调用。
A Flow is a stream of data over time. csrfToken,例如,是一个字符串的Flow,你可以使用 collect 对将被Flow发出的每一个值执行某个动作——不仅仅是在第一个值或当前值等情况。值在DataStore Flow中被发出,当DataStore发生变化时(也就是说,saveCsrf 或 saveSession 被调用)。
Although collect will start working immediately by executing its body (here updating cookieString), it will then wait until the next value was emitted into the Flow to execute the body again. That's why it is blocking the rest of your code. Flows can end naturally, but a DataStore Flow does not: For as long as the DataStore is active (most likely the entire lifetime of your app), its Flow will be active, emitting new values when anything changes. collect will therefore suspend indefinitely because there will never be a last value.
This is intended behavior and is a basic principle of how Kotlin Flows work.
The solution to your problem is to handle the Flows differently. If you do not want (or need) the feature that Flows can reactively inform you about new values, you can simply cancel the collection after the first value was received. That's what first() does under the hood: Simply replace all occurrences of collect with first().let。
getCsrfToken() can then be simplified even more to just this:
suspend fun getCsrfToken(): String = csrfToken.first()
And this is how getCookiesAsString() would look like:
suspend fun getCookiesAsString(): String {
var cookieString = ""
csrfToken.first().let {
if (it != "No Cookie") {
cookieString = cookieString.plus("csrftoken=$it;")
}
}
sessionToken.first().let {
if (it != "No Cookie") {
cookieString = cookieString.plus("sessionid=$it;")
}
}
return cookieString.dropLast(1)
}
Since both the csrfToken Flow and the sessionToken Flow are based on the same Flow, though, this can be simplified by accessing the underlying dataStore.data directly:
suspend fun getCookiesAsString(): String {
val preferences = dataStore.data.first()
return listOfNotNull(
preferences[CSRF]?.let { "csrftoken=$it" },
preferences[SESSION]?.let { "sessionid=$it" },
).joinToString(";")
}
This also forgoes the error handling which I'm not sure why that was even there in the first place. If the DataStore cannot access the file system there is something seriously wrong that you shouldn't just ignore by handing out empty preferences. You should probably just crash the app instead so that the user can try to fix the underlying problem, for example by reinstalling the app or at least wiping its data.