MVVM 架构与 Jetpack AAC
mediumMVVMViewModelLiveDataRepositoryHilt
为什么需要架构
没有架构的 Android 项目,代码会堆在 Activity/Fragment 里:网络请求、数据库操作、业务逻辑、UI 更新全混在一起,很快变成"上帝类"。结果是:
- 代码难以测试(无法脱离 Android 环境单元测试)
- 屏幕旋转(Activity 重建)导致数据丢失或重复请求
- 逻辑和 UI 高度耦合,改一处牵一发动全身
MVVM(Model-View-ViewModel) 解决了上述问题,是 Google 官方推荐的 Android 架构模式。
MVVM 各层的职责
┌─────────────────────────────────────────────────────┐
│ View(Activity / Fragment / Composable) │
│ 职责:展示 UI,采集用户输入,观察 ViewModel │
│ ⚠️ 不写业务逻辑,不直接访问数据源 │
└────────────────────────┬────────────────────────────┘
│ 观察 StateFlow/LiveData
│ 调用 ViewModel.xxx()
┌────────────────────────▼────────────────────────────┐
│ ViewModel │
│ 职责:持有 UI 状态,处理用户事件,协调数据层 │
│ ⚠️ 不直接引用 Context(用 applicationContext) │
│ ⚠️ 不直接引用 View │
│ 生命周期:比 Activity 长,屏幕旋转不销毁 │
└────────────────────────┬────────────────────────────┘
│ 调用 Repository
┌────────────────────────▼────────────────────────────┐
│ Repository │
│ 职责:单一数据入口,决策从哪里取数据(网络/缓存/DB) │
│ ⚠️ ViewModel 不知道数据来自网络还是数据库 │
└───────────┬──────────────────────────┬──────────────┘
│ │
┌───────────▼──────┐ ┌───────────▼──────────────┐
│ Remote(API) │ │ Local(Room/DataStore) │
│ Retrofit 接口 │ │ DAO / SharedPreferences │
└──────────────────┘ └──────────────────────────┘
ViewModel:跨越配置变更
ViewModel 由 ViewModelStore 持有,ViewModelStore 在屏幕旋转时不随 Activity 销毁。只有用户真正退出页面(按返回键或调用 finish()),ViewModel 才会被清理(onCleared() 被调用)。
// 定义 ViewModel
class UserListViewModel(
private val userRepository: UserRepository
) : ViewModel() {
// UI 状态:外部只读,内部可写
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 搜索关键词
private val _searchQuery = MutableStateFlow("")
// 基于搜索词过滤的用户列表(Flow 联动)
val users: StateFlow<List<User>> = _searchQuery
.debounce(300) // 防抖:300ms 内不重复搜索
.flatMapLatest { query ->
userRepository.searchUsers(query)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
init {
loadUsers()
}
fun loadUsers() {
_uiState.value = UiState.Loading
viewModelScope.launch {
try {
userRepository.refreshUsers() // 从网络更新本地数据库
_uiState.value = UiState.Success
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "加载失败")
}
}
}
fun onSearchQueryChanged(query: String) {
_searchQuery.value = query
}
override fun onCleared() {
super.onCleared()
// 可以在这里释放资源
}
}
// View(Fragment)
class UserListFragment : Fragment() {
private val viewModel: UserListViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 搜索框联动
searchInput.addTextChangedListener { text ->
viewModel.onSearchQueryChanged(text.toString())
}
// 收集 UI 状态(配合生命周期自动取消)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.uiState.collect { state ->
when (state) {
UiState.Loading -> showLoading()
UiState.Success -> hideLoading()
is UiState.Error -> showError(state.message)
}
}
}
launch {
viewModel.users.collect { users ->
adapter.submitList(users)
}
}
}
}
}
sealed class UiState {
object Loading : UiState()
object Success : UiState()
data class Error(val message: String) : UiState()
}
}
Repository:单一数据源
Repository 是数据层的门面(Facade),ViewModel 只和 Repository 交互,不关心数据从哪里来:
class UserRepository(
private val userApi: UserApi, // 远程数据源
private val userDao: UserDao // 本地数据源
) {
// 推荐模式:本地为主,网络同步
// Flow 直接来自 Room,Room 数据变化时自动推送
fun searchUsers(query: String): Flow<List<User>> {
return userDao.searchUsers(query)
}
// 主动刷新:从网络取最新数据存入 Room,Room 变化后 Flow 自动通知 UI
suspend fun refreshUsers() {
val remoteUsers = userApi.getUsers()
userDao.insertAll(remoteUsers.map { it.toEntity() })
}
// 通过 ID 获取单个用户(先查本地,不存在再请求网络)
suspend fun getUser(id: String): User {
return userDao.getUserById(id) ?: run {
val remote = userApi.getUser(id)
userDao.insertUser(remote.toEntity())
remote.toEntity()
}
}
}
Hilt:依赖注入简化 ViewModel
手动创建 Repository 和 ViewModel 很繁琐,Hilt 是 Google 推荐的 DI 框架:
// 1. 标记 Application
@HiltAndroidApp
class MyApplication : Application()
// 2. 提供依赖(Module)
@Module
@InstallIn(SingletonComponent::class) // 应用范围的单例
object DatabaseModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "app_db").build()
}
@Provides
fun provideUserDao(database: AppDatabase): UserDao = database.userDao()
}
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideRetrofit(): Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)
}
// 3. 注入 Repository
@Singleton
class UserRepository @Inject constructor(
private val userApi: UserApi,
private val userDao: UserDao
)
// 4. 注入 ViewModel
@HiltViewModel
class UserListViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel()
// 5. 在 Fragment 中获取 ViewModel(零手动创建)
@AndroidEntryPoint
class UserListFragment : Fragment() {
private val viewModel: UserListViewModel by viewModels()
// Hilt 自动注入所有依赖
}
SavedStateHandle:处理进程死亡
ViewModel 能活过屏幕旋转,但进程被系统杀死后 ViewModel 也会消失。SavedStateHandle 能在进程死亡后恢复少量关键状态:
@HiltViewModel
class DetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, // Hilt 自动注入
private val repository: UserRepository
) : ViewModel() {
// 从 Navigation 参数或 savedState 中获取 userId
private val userId: String = savedStateHandle.get<String>("userId")!!
// 搜索词在进程死亡后也能恢复
var searchQuery: StateFlow<String> = savedStateHandle.getStateFlow("query", "")
fun setQuery(query: String) {
savedStateHandle["query"] = query // 自动持久化
}
}
选择 SavedStateHandle 还是 Room:
- 少量 UI 状态(如滚动位置、搜索词)→ SavedStateHandle
- 大量业务数据 → Room 数据库
DataStore:替代 SharedPreferences
SharedPreferences 是同步 I/O,在主线程可能导致 ANR,且不支持类型安全。DataStore 是基于协程和 Flow 的现代替代品:
// Preferences DataStore(KV 存储,类似 SharedPreferences)
val Context.dataStore: DataStore<Preferences> by preferencesDataStore("settings")
class SettingsRepository(private val dataStore: DataStore<Preferences>) {
companion object {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val USER_ID = stringPreferencesKey("user_id")
}
// 读取(Flow 自动在数据变化时更新)
val darkMode: Flow<Boolean> = dataStore.data.map { prefs ->
prefs[DARK_MODE] ?: false
}
// 写入(挂起函数,在 IO 线程执行)
suspend fun setDarkMode(enabled: Boolean) {
dataStore.edit { prefs ->
prefs[DARK_MODE] = enabled
}
}
}