状态管理
app/src/main/java/com/joker/kit/core/state 当前只维护 UserState。它集中保存登录结果、用户资料、Token 等关键信息,并通过 StateFlow 向任意页面实时推送。目录下的 di/AppStateModule.kt 为其注入 @ApplicationScope 协程,保证在应用进程生命周期内都能响应事件。若后续出现订单、购物车等其他需要全局共享的状态,亦可在该目录新增对应的状态持有者,沿用同样的模式。
核心职责
- 全局单一数据源:
isLoggedIn、userInfo、auth等StateFlow在Application初始化时即加载本地缓存,之后任何页面订阅即可收到更新,避免 A 页修改资料后 B 页不同步的问题。 - 桥接多数据来源:
UserState同时依赖AuthStoreRepository(Token/MMKV)、UserInfoStoreRepository(本地资料)与UserInfoRepository(网络接口),统一封装持久化与同步流程。 - 协程托管:通过
@ApplicationScope在应用级CoroutineScope内收集网络 Flow、写入本地并推送 UI,外部调用无需关心线程与生命周期。 - 与导航/基类联动:所有
BaseViewModel子类都持有userState,可直接查询登录状态以决定是否跳转、是否拦截路由,实现“全局登录态 + 局部 UI”联动。
初始化流程
UserState 不会在构造时立刻读取本地,而是由 Application 在完成基础依赖(如 MMKV)后手动调用 initialize()。Hilt 模块负责提供应用级协程:
@Module
@InstallIn(SingletonComponent::class)
object AppStateModule {
@ApplicationScope
@Provides @Singleton
fun providesApplicationScope(): CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Default)
}@HiltAndroidApp
class Application : Application() {
@Inject lateinit var userState: UserState
override fun onCreate() {
super.onCreate()
MMKVUtils.init(this)
userState.initialize() // 等待 MMKV 准备好后再恢复登录状态
}
}完成初始化后,UserState 会读取本地的 Auth、User 信息,填充 StateFlow 并决定是否已登录,业务层无需重复读取存储或处理异常。
在页面中订阅与刷新
任何 BaseViewModel 子类都可以直接使用受保护的 userState。示例:
@HiltViewModel
class ProfileViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState
) : BaseViewModel(navigator, userState) {
val isLoggedIn: StateFlow<Boolean> = userState.isLoggedIn
val userInfo: StateFlow<User?> = userState.userInfo
fun refreshProfile() {
userState.refreshUserInfo()
}
}界面层只需收集这些 StateFlow 即可实现响应式刷新:
@Composable
fun ProfileRoute(viewModel: ProfileViewModel = hiltViewModel()) {
val isLoggedIn by viewModel.isLoggedIn.collectAsState()
val userInfo by viewModel.userInfo.collectAsState()
if (!isLoggedIn) {
LoginHint(onClick = { /* 跳转登录 */ })
} else {
Text(text = userInfo?.nickname.orEmpty())
}
}登录成功或用户修改昵称时,只需调用 userState 提供的写入方法,所有订阅者都会同时得到更新,无需手动通知多个页面。
写入策略示例
- 登录成功:在登录接口成功后调用
updateUserState(auth, user),方法内部会先写入AuthStoreRepository/UserInfoStoreRepository,再同步内存状态并把isLoggedIn置为true。 - 刷新 Token:当检测到 Token 将过期时,可以调用
updateAuth(newAuth)覆盖本地缓存,而无需重新获取用户资料。 - 编辑资料:调用
updateUserInfo(newUser),既写入本地,也会更新_userInfo+_userId,其他页面立刻收到最新头像/昵称。 - 手动同步网络:
refreshUserInfo()会在ApplicationScope内触发UserInfoRepository.getPersonInfo(),并复用ResultHandler.handleResultWithData处理结果与异常。 - 退出登录:
logout()统一清理本地缓存并重置所有StateFlow,UI 自动回到未登录状态。
扩展其他共享状态示例
当业务需要新增“跨页面共享状态”时(例如购物车角标、App 级配置等),可以在 core/state 下创建新的状态持有类,并参考 UserState 的依赖注入方式。以下示例演示如何新增 CartState,用于在多个页面同步购物车数量:
1. 创建状态类(core/state/cart/CartState.kt)
@Singleton
class CartState @Inject constructor(
private val cartRepository: CartRepository,
@ApplicationScope private val appScope: CoroutineScope
) {
private val _cartCount = MutableStateFlow(0)
val cartCount: StateFlow<Int> = _cartCount.asStateFlow()
fun observeCart() {
appScope.launch {
cartRepository.observeCart()
.collect { list -> _cartCount.value = list.sumOf { it.count } }
}
}
}2. 提供依赖:由于 CartState 带有 @Singleton 与 @Inject 构造函数,Hilt 会自动生成依赖;只要 CartRepository、ApplicationScope 等依赖可被注入,即可在任意组件中直接声明 @Inject constructor(..., private val cartState: CartState)。
3. 使用方式:在需要展示购物车数量的 ViewModel 中注入 CartState,并收集 cartCount:
@HiltViewModel
class CartBadgeViewModel @Inject constructor(
navigator: AppNavigator,
userState: UserState,
private val cartState: CartState
) : BaseViewModel(navigator, userState) {
val cartCount = cartState.cartCount
init { cartState.observeCart() }
}当新增其他共享状态时,只需替换仓库依赖与状态字段,即可获得与 UserState 一致的响应式体验。
API 参考
状态流
| 名称 | 类型 | 说明 |
|---|---|---|
isLoggedIn | StateFlow<Boolean> | 是否已登录,默认 false |
userId | StateFlow<Long> | 当前用户 ID,未登录为 0L |
auth | StateFlow<Auth?> | 当前 Token/过期时间等认证信息 |
userInfo | StateFlow<User?> | 用户资料(头像、昵称、性别等),可能为 null |
写入/操作方法
| 名称 | 主要参数 | 说明 |
|---|---|---|
initialize() | - | 读取本地缓存初始化状态,需在 Application 中手动调用一次 |
updateUserState(auth, user) | Auth, User | 登录成功后调用,写入本地并推送所有状态流 |
updateUserInfo(user) | User | 编辑资料后调用,仅更新用户信息相关流 |
updateAuth(auth) | Auth | Token 刷新或更换设备时调用,同时保持登录态 |
logout() | - | 清除本地缓存和内存状态,恢复到未登录 |
shouldRefreshToken() | suspend fun | 读取本地标记判断 Token 是否需要刷新,可结合拦截器使用 |
refreshUserInfo() | - | 触发网络请求刷新资料,内部使用 ResultHandler 自动处理 Loading/错误 |
借助 UserState,数据拉取与跨页面同步被收敛到一个文件中:登录页、个人中心、导航栏角标等都可以共享同一份响应式数据,避免重复编写“取本地缓存 -> 解析 -> 通知 UI”的代码,也让全局登录态更容易维护。