Android 中 ViewModel、Application、Repository 与 currentUser 的关系
一、ViewModel 不是全局数据仓库
在 Android 开发中,ViewModel 主要用于保存和管理 页面相关的 UI 状态。
例如:
class ProfileViewModel : ViewModel() {
val userName = MutableLiveData<String>()
}
ViewModel 的生命周期不是全局的,而是绑定到某个作用域上的。
常见作用域包括:
Activity 作用域
Fragment 作用域
Navigation Graph 作用域
也就是说,ViewModel 能不能共享,关键不在于类名,而在于它绑定到了哪个 ViewModelStoreOwner。
二、SharedViewModel 不是特殊类型
开发中经常会看到 SharedViewModel 这个名字,但它并不是 Android 提供的特殊 ViewModel。
例如:
class SharedViewModel : ViewModel() {
val selectedItem = MutableLiveData<String>()
}
这里的 SharedViewModel 只是开发者自己起的名字。
真正让它“共享”的是作用域,比如:
private val viewModel: SharedViewModel by activityViewModels()
如果多个 Fragment 都使用 activityViewModels(),那么它们拿到的是同一个 Activity 作用域下的 ViewModel。
例如:
MainActivity
├── ListFragment
└── DetailFragment
两个 Fragment 都这样写:
private val viewModel: SharedViewModel by activityViewModels()
这时它们共享的是同一个 ViewModel 实例。
但是如果两个 Fragment 都这样写:
private val viewModel: SharedViewModel by viewModels()
那么它们拿到的是各自独立的 ViewModel 实例。
所以可以记住一句话:
SharedViewModel 不是特殊 ViewModel;
它只是普通 ViewModel + 更大的作用域。
三、AndroidViewModel 也不是 Application 作用域
有些人会想到 AndroidViewModel:
class MyViewModel(application: Application) : AndroidViewModel(application)
AndroidViewModel 的特点是可以拿到 Application 对象。
但是它本身并不代表 Application 作用域。
如果在 Activity 中使用:
private val viewModel: MyViewModel by viewModels()
它仍然是 Activity 作用域。
如果在 Fragment 中使用:
private val viewModel: MyViewModel by viewModels()
它仍然是 Fragment 作用域。
所以:
AndroidViewModel = 可以拿到 Application Context 的 ViewModel
不是 = Application 级别的全局 ViewModel
四、以前为什么很多共享数据直接放 Application?
以前很多 Android 项目确实喜欢把共享数据直接放在 Application 里。
例如:
class MyApp : Application() {
var token: String? = null
var currentUser: User? = null
}
然后在各个地方这样使用:
val app = application as MyApp
val user = app.currentUser
这种写法确实可以工作。
早期项目中,常见结构比较简单:
Activity
Fragment
Application
AsyncTask
Handler
那时 ViewModel、Repository、Room、DataStore、Hilt 等架构组件还没有现在这么普及,所以 Application 很容易被当成“全局变量箱子”。
常见放进去的数据包括:
登录状态
token
currentUser
全局配置
SDK 管理对象
购物车缓存
定位信息
设备信息
五、为什么现在不推荐把业务数据直接放 Application?
Application 的生命周期接近整个应用进程。
如果把大量可变业务数据都放在 Application 中,会带来很多问题。
例如:
class MyApp : Application() {
var currentUser: User? = null
}
这种写法的问题是:
谁都可以改
什么时候改的不清楚
什么时候清理不清楚
测试困难
页面之间耦合严重
进程被杀后数据会丢失
比如 Activity A 修改了 currentUser,Activity B 也修改了 currentUser,Service 也可能修改它。
时间久了就很难判断:
这个数据是谁改的?
什么时候改的?
为什么现在是这个值?
进程重启后怎么办?
所以现在更推荐把 Application 当成:
全局初始化入口
依赖容器入口
全局 Context 提供者
而不是直接当作业务数据仓库。
六、Application 更适合做什么?
Application 适合做一些全局初始化工作。
例如:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
// 初始化日志库
// 初始化崩溃收集 SDK
// 初始化推送 SDK
// 初始化数据库
// 初始化依赖注入框架
}
}
也可以作为依赖容器的入口:
class MyApp : Application() {
lateinit var appContainer: AppContainer
}
class AppContainer(context: Context) {
val userRepository = UserRepository(...)
val authRepository = AuthRepository(...)
}
这种方式比直接把 currentUser、token 放在 Application 里更清晰。
七、Repository 是什么?
Repository 不是具体的存储位置。
它更像是一个“数据管家”。
它负责统一管理数据来源。
数据可能来自:
内存缓存
DataStore
Room 数据库
网络接口
文件
其他 SDK
ViewModel 不需要关心数据到底来自哪里,只需要向 Repository 要数据。
结构通常是:
Activity / Fragment
↓
ViewModel
↓
Repository
↓
DataStore / Room / Network / Memory Cache
八、currentUser 放在 Repository,具体存在哪里?
currentUser 放在 Repository,并不代表一定要放数据库。
它可能有几种存储方式。
九、方式一:只存在内存中
适合 App 运行期间的临时缓存。
class UserRepository {
private val _currentUser = MutableStateFlow<User?>(null)
val currentUser: StateFlow<User?> = _currentUser
fun setCurrentUser(user: User) {
_currentUser.value = user
}
fun logout() {
_currentUser.value = null
}
}
这种方式简单,但缺点是:
App 进程被杀后,数据就没了。
所以只适合临时状态。
十、方式二:存到 DataStore
DataStore 适合保存轻量级数据。
例如:
token
refreshToken
userId
登录状态
用户设置
是否首次启动
示例:
class AuthRepository(
private val dataStore: DataStore<Preferences>
) {
val tokenFlow: Flow<String?> = dataStore.data.map { prefs ->
prefs[stringPreferencesKey("token")]
}
suspend fun saveToken(token: String) {
dataStore.edit { prefs ->
prefs[stringPreferencesKey("token")] = token
}
}
suspend fun clearToken() {
dataStore.edit { prefs ->
prefs.remove(stringPreferencesKey("token"))
}
}
}
这种方式适合保存登录凭证、用户 ID 等轻量数据。
十一、方式三:存到 Room 数据库
Room 适合保存结构化数据。
例如:
用户资料
联系人
订单
聊天记录
收藏列表
文章缓存
用户信息可以设计成实体类:
@Entity
data class UserEntity(
@PrimaryKey val id: Long,
val name: String,
val avatar: String
)
Repository 中统一管理:
class UserRepository(
private val userDao: UserDao,
private val api: UserApi
) {
fun observeCurrentUser(userId: Long): Flow<UserEntity?> {
return userDao.observeUser(userId)
}
suspend fun refreshUser(userId: Long) {
val user = api.getUser(userId)
userDao.insert(user.toEntity())
}
}
这种方式适合保存比较完整的用户资料。
十二、真实项目中常见组合
实际项目里,通常不是只用一种方式,而是组合使用。
比较常见的设计是:
DataStore 保存 token、refreshToken、userId
Room 保存用户详细资料
Network 获取最新用户信息
Repository 统一管理这些数据
结构大概是:
AuthRepository
└── DataStore
├── token
├── refreshToken
└── userId
UserRepository
├── Room
│ └── UserEntity
└── Network
└── getUserInfo()
登录成功后:
1. 保存 token 到 DataStore
2. 保存 userId 到 DataStore
3. 请求用户信息
4. 把用户信息缓存到 Room
5. 页面通过 Flow 自动收到 currentUser
退出登录时:
1. 清除 DataStore 里的 token、userId
2. 清除 Room 里的用户缓存
3. currentUser 变成 null
十三、currentUser 可以不是一个变量,而是一个数据流
现代写法中,currentUser 不一定是一个普通变量。
它可以是一个 Flow。
例如:
class UserRepository(
private val authRepository: AuthRepository,
private val userDao: UserDao
) {
val currentUser: Flow<User?> =
authRepository.userIdFlow.flatMapLatest { userId ->
if (userId == null) {
flowOf(null)
} else {
userDao.observeUser(userId)
}
}
}
这个逻辑表示:
先从 DataStore 获取当前登录用户的 userId
再根据 userId 从 Room 查询用户信息
最后暴露成 currentUser
这样 ViewModel 只需要订阅:
val currentUser = userRepository.currentUser
而不用关心数据到底来自 DataStore、Room 还是网络。
十四、ViewModel、Application、Repository 的职责区别
十五、总结
以前很多项目会把共享数据直接放在 Application 里,这种方式简单直接,但容易造成全局变量滥用。
现在更推荐的方式是:
Application 负责全局初始化和依赖入口;
ViewModel 负责页面状态;
Repository 负责统一管理数据;
DataStore / Room / Network 负责具体数据来源。
关于 currentUser,可以这样记:
临时用户状态:可以放内存
token / userId:适合放 DataStore
完整用户资料:适合放 Room
最新用户信息:从 Network 获取
统一管理入口:放在 Repository
一句话总结:
Repository 是数据管家,不是数据本体;
Application 是应用入口,不是全局变量箱子;
ViewModel 是页面状态管理者,不是全局数据仓库。
Android 中 ViewModel、Application、Repository 与 currentUser 的关系
https://lautung.com/archives/XpcsvO3n