feat: add provider login flow #4
@@ -32,9 +32,11 @@ class LoginViewModel @Inject constructor(
|
||||
private var usernameEdited = false
|
||||
private var rememberUsernameEdited = false
|
||||
private var autoLoginEdited = false
|
||||
|
|
||||
private var loadedProfileId: com.flagship.abox.manager.domain.ProviderProfileId? = null
|
||||
|
||||
fun loadProfile(value: ProviderProfile) {
|
||||
|
pr-agent-bot
commented
状态丢失
**状态丢失**
`LoginRoute` 重新进入组合时会再次调用 `loadProfile`。当同一提供方已加载完成且当前没有任务时,此实现仍会重置编辑标记并用新的 `LoginUiState` 覆盖现有状态。因此 Activity 因旋转等配置变更重建时,虽然 ViewModel 得以保留,用户已输入的用户名和密码仍会被清空。应对已加载的相同配置直接返回,或在刷新时保留当前编辑值。
<!-- pr-agent-dedup: 3e11ec9ce7f1 -->
<!-- pr-agent-key-issue-location: 75605d605f67 -->
|
||||
if (profile?.id == value.id &&
|
||||
if (loadedProfileId == value.id ||
|
||||
profile?.id == value.id &&
|
||||
(loadingJob?.isActive == true || submitJob?.isActive == true)
|
||||
|
pr-agent-bot
commented
输入被覆盖
**输入被覆盖**
`loginPreferences` 异步读取完成后会直接替换整个 `LoginUiState`。如果用户在 DataStore 读取期间已输入用户名或密码,这些输入会被已保存的偏好无条件清空或覆盖。建议加载期间禁用输入,或仅在对应字段尚未被用户修改时合并偏好值。
<!-- pr-agent-dedup: 0d8efc2f0a77 -->
<!-- pr-agent-key-issue-location: 043e23a2b991 -->
|
||||
) return
|
||||
loadingJob?.cancel()
|
||||
@@ -65,6 +67,7 @@ class LoginViewModel @Inject constructor(
|
||||
},
|
||||
isLoadingPreferences = false,
|
||||
)
|
||||
loadedProfileId = value.id
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (_: Exception) {
|
||||
@@ -92,6 +95,7 @@ class LoginViewModel @Inject constructor(
|
||||
return
|
||||
}
|
||||
profile = null
|
||||
loadedProfileId = null
|
||||
usernameEdited = false
|
||||
rememberUsernameEdited = false
|
||||
autoLoginEdited = false
|
||||
|
||||
@@ -28,12 +28,9 @@ class ProviderProfileStoreImpl(
|
||||
private val safeData: Flow<Preferences>
|
||||
|
pr-agent-bot
commented
无限重试
**无限重试**
`safeData` 遇到任何 `IOException` 都会无限重试,没有最大次数或最终错误路径。如果 DataStore 因磁盘或文件系统故障而持续不可读,`activeProfile()` 和 `loginPreferences()` 将永远挂起,导致应用一直停留在加载页或登录表单持续禁用,上层异常处理也无法展示错误。应限制重试次数,并在耗尽后抛出错误或执行明确的降级处理。
<!-- pr-agent-dedup: 4389fff2f610 -->
<!-- pr-agent-key-issue-location: b2da98684d7a -->
pr-agent-bot
commented
启动失败
**启动失败**
`safeData` 在两次重试后仍会向调用方传播 `IOException`,而启动流程中的 `activeProfile()` 和 `observeActiveProfile()` 均未处理该异常。持续性存储读取故障会触发未处理异常;即使异常被全局处理,`bootstrapResult` 也可能一直为空,使应用永久停留在加载页。应在重试耗尽后将错误转换为可恢复状态或显式领域错误。
<!-- pr-agent-dedup: 7302d3a589b8 -->
<!-- pr-agent-key-issue-location: db2a740c074a -->
pr-agent-bot
commented
启动卡死 当 DataStore 连续读取失败并耗尽重试后, **启动卡死**
当 DataStore 连续读取失败并耗尽重试后,`safeData` 会继续抛出 `IOException`。`observeActiveProfile()` 是 `combine(...).stateIn(...)` 的上游之一,该异常会终止共享协程,导致启动界面永久停留在加载状态或后续一直保留陈旧状态。应在共享前捕获并映射为可恢复错误,或采用不会终止状态流的重试策略。
<!-- pr-agent-dedup: 31fd4d3bef7b -->
<!-- pr-agent-key-issue-location: 2275a87bf139 -->
|
||||
get() = dataStore.data
|
||||
|
pr-agent-bot
commented
无限重试 达到 **无限重试**
达到 `MAX_READ_RETRIES` 后虽然发射了空配置,但谓词仍返回 `true`,因此持续性 `IOException` 会让每个长期订阅者以约 50 毫秒的间隔无限重读并反复发射空配置,造成电量消耗和会话界面抖动。达到上限后应停止重试,并通过 `catch` 等方式仅发射一次回退值。
<!-- pr-agent-dedup: 0b718cc7f1c2 -->
<!-- pr-agent-key-issue-location: 701272b74db5 -->
|
||||
.retryWhen { cause, attempt ->
|
||||
|
pr-agent-bot
commented
观察流终止 当 **观察流终止**
当 `DataStore` 的读取 `IOException` 超过重试上限时,`catch` 发出一次空首选项后会结束整个冷流。`observeActiveProfile()` 等长期订阅不会在存储恢复后继续接收更新;例如用户重新选择提供方且写入成功,应用仍可能停在提供方选择界面,直到进程重启。应让观察流继续重试或重新订阅,而不是以空值结束。
<!-- pr-agent-dedup: 1ff2c2415377 -->
<!-- pr-agent-key-issue-location: cf47b7bb3191 -->
|
||||
if (cause !is IOException) return@retryWhen false
|
||||
val delayMillis = (READ_RETRY_DELAY_MILLIS shl
|
||||
attempt.coerceAtMost(MAX_BACKOFF_SHIFT).toInt())
|
||||
.coerceAtMost(MAX_READ_RETRY_DELAY_MILLIS)
|
||||
delay(delayMillis)
|
||||
true
|
||||
val retry = cause is IOException && attempt < MAX_READ_RETRIES
|
||||
if (retry) delay(READ_RETRY_DELAY_MILLIS)
|
||||
retry
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
@@ -118,9 +115,8 @@ class ProviderProfileStoreImpl(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_READ_RETRIES = 2L
|
||||
private const val READ_RETRY_DELAY_MILLIS = 100L
|
||||
private const val MAX_READ_RETRY_DELAY_MILLIS = 5_000L
|
||||
private const val MAX_BACKOFF_SHIFT = 6L
|
||||
private val PROFILE_IDS = stringSetPreferencesKey("profile_ids")
|
||||
private val ACTIVE_PROFILE_ID = stringPreferencesKey("active_profile_id")
|
||||
|
||||
|
||||
隐私残留
loadingJob完成后从未置空,却被用作“已经加载”的判断。登录成功后退出到同一提供方时,Activity 级 ViewModel 会被复用,新的路由加载会直接返回;即使用户未选择“记住用户名”,上次输入的用户名仍会重新显示。应判断任务是否仍处于活动状态,或在finally中清空任务并按持久化偏好重置表单。