安卓学习笔记-数据存储
阅读说明
本文是基于上一篇文章《安卓学习笔记-声明式UI》的后续。上篇文章实现了UI
层以及业务逻辑层ViewModel
的解耦。本篇关注的是数据存储层与业务逻辑层的解耦。
补充知识StateFlow
在 MVVM 架构中如何使用 Kotlin 协程的 StateFlow 来管理和暴露 UI 状态。
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
_uiState
(MutableStateFlow) 允许在 ViewModel 内部修改 UI 状态。
uiState
(StateFlow) 只向外部(UI 层,如 Composable)暴露一个只读的流。
这意味着 UI 层只能观察状态的变化并更新自身,但它不能直接修改状态。如果您直接将 MutableStateFlow
暴露给 UI,那么 UI 层(例如 Composable 函数)可能会错误地直接修改 ViewModel 的状态,而不是通过 ViewModel 定义的公共方法(如 onLoginClick()
)来修改。这会导致:
- 逻辑混乱: 难以追踪状态是如何改变的,因为改变可能发生在任何地方。
- 违反 MVVM 模式: MVVM 强调 ViewModel 是 UI 状态的唯一管理者和业务逻辑的执行者,UI 只是状态的消费者。
- 难以调试: 当出现 Bug 时,定位问题变得困难。
数据存储实现
MVVM 架构中常见的实践用 Repository 来做数据的获取/存储,而 ViewModel 专注于业务逻辑和状态管理,类似Java中的Dao
与Service
用一个完整示例说明:用 ViewModel + Repository 实现用户输入数据的保存与读取(例如用户名保存到本地数据源)。
示例:保存用户名(本地存储)
Repository 接口定义
interface UserRepository {suspend fun getUsername(): Stringsuspend fun saveUsername(name: String)
}
如果 ViewModel 直接依赖具体实现(例如 UserRepositoryImpl
),那么 ViewModel 就与这个实现类强耦合,未来很难替换或扩展。
通过接口,ViewModel 只依赖于抽象(UserRepository
),不关心具体的数据来源。这种解耦带来极大的灵活性,比如我们可以:
- 开发阶段使用模拟数据(MockRepository),快速验证 UI 和交互逻辑;
- 后期轻松切换到 本地持久化方案(如使用 DataStore 的
UserRepositoryImpl
); - 或进一步接入 远程网络服务(如调用 Retrofit 接口),只需提供一个新的实现类即可。
这样,ViewModel 的代码完全不用修改,极大提升了代码的可维护性与可扩展性。
Repository 实现(本地存储)
对于存储键值对类型的数据,例如:用户名、是否开启夜间模式、上次登录时间等。常见的可以用SharedPreferences、DataStore来实现。这里以DataStore为例进行说明,不用SharedPreferences
的原因如下:
SharedPreferences 的问题
- 同步执行:读写都默认是同步操作,可能阻塞主线程
- 并发不安全:多线程同时访问时,容易出错
- 不支持 Flow / 响应式:不能自动监听变化
- 不推荐在 Jetpack Compose 中使用
DataStore介绍
DataStore 是一种替代 SharedPreferences 的方式,用于存储键值对或结构化数据,具有更高的安全性和性能。
分为两种:
Preferences DataStore
键值对存储(像SharedPreferences
)Proto DataStore
使用ProtoBuf
存储结构化数据
这里以Preferences DataStore
为例
Preferences DataStore用法
添加依赖:
implementation("androidx.datastore:datastore-preferences:1.0.0")
初始化 DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")
代码说明
val Context.dataStore
这是 给 Context
类扩展一个属性,也就是说,之后你就可以像这样使用
context.dataStore // 获取 DataStore 实例
this.dataStore // 如果在 Activity、Application 中
preferencesDataStore()
是 Jetpack 提供的一个函数。它的返回值是一个属性委托对象。这个对象负责
- 在第一次访问
context.dataStore
时创建 DataStore 实例 - 将这个实例缓存下来,保证全局只有一个单例(singleton)
- 之后每次访问,返回的都是同一个 DataStore 实例
写入数据
val USERNAME_KEY = stringPreferencesKey("username")suspend fun saveUsername(context: Context, name: String) {context.dataStore.edit { prefs ->prefs["username"] = name}
}
读取数据
suspend fun getUsername(context: Context): String {val prefs = context.dataStore.data.first()return prefs["username"] ?: "默认用户名"
}
或者监听变化(响应式 Flow):
val usernameFlow: Flow<String> = context.dataStore.data.map { it["username"] ?: "默认用户名" }
实现UserRepository
接口
添加依赖
implementation("androidx.datastore:datastore-preferences:1.0.0")
创建 Context.dataStore
扩展
创建util
包,然后新增DataStore.kt
文件
package com.wy.demo.utilimport android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore// 扩展属性:Context.userDataStore
val Context.userDataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs" // DataStore 存储文件名,对应 files/datastore/user_prefs.preferences_pb
)
实现UserRepository
接口
创建UserRepositoryImpl
类
package com.wy.demo.data.repository import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import com.wy.demo.util.userDataStore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map class UserRepositoryImpl( private val context: Context
) : UserRepository { private val USERNAME_KEY = stringPreferencesKey("username") override suspend fun getUsername(): String { return context.userDataStore.data .map { preferences -> preferences[USERNAME_KEY] ?: "默认用户名" } .first() } override suspend fun saveUsername(name: String) { context.userDataStore.edit { preferences -> preferences[USERNAME_KEY] = name } }
}
DataStore 是类型安全的,所以需要一个“类型安全的键对象”。stringPreferencesKey("username")
创建了一个 类型安全的、用于存储字符串的键,后续用它来读写 DataStore 里的用户名。
修改DemoViewModel
package com.wy.demo.viewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wy.demo.data.repository.UserRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch class DemoViewModel(private val repo: UserRepository) : ViewModel() { private val _name = MutableStateFlow("") val name: StateFlow<String> = _name init { viewModelScope.launch { _name.value = repo.getUsername() } } fun updateName(newName: String) { viewModelScope.launch { repo.saveUsername(newName) _name.value = newName } }
}
代码说明
初始化代码块init{ ... }
每当 ViewModel
实例被创建时,这段代码就会自动执行一次。
viewModelScope.launch { ... }
viewModelScope 是 ViewModel 提供的一个 协程作用域,生命周期与 ViewModel 绑定。其意义在于:在 ViewModel 的生命周期范围内安全地启动后台任务,任务自动管理,无需手动取消,防止内存泄漏或数据回调异常。
如何创建UserRepository对象
Hilt 是 Google 推出的 Android 官方依赖注入框架(DI 框架),它是基于 Dagger 构建的简化版,专为 Android 应用设计。Hilt 可以够像使用Spring那样自动帮你创建对象并注入依赖,让你的代码更简洁、模块更解耦。这里使用Hilt来解决对象创建以及依赖问题。以下是Hilt 使用步骤
步骤 1:添加 Hilt 依赖 (Gradle 配置)
在您的项目根目录的 build.gradle.kts
(Project 级别) 文件中添加 Hilt Gradle 插件:
id("com.google.dagger.hilt.android") version "2.51.1" apply false // Hilt Gradle 插件
然后,在您的应用模块的 build.gradle.kts
(Module: app 级别) 文件中应用 Hilt 插件并添加依赖:
// app/build.gradle.kts (Module 级别)
plugins {id("com.google.dagger.hilt.android") // 应用 Hilt 插件kotlin("kapt") // 应用 Kotlin Annotation Processing Tool 插件
}android {// ...
}dependencies {// ....省略// Hilt 核心依赖 implementation("com.google.dagger:hilt-android:2.51.1") kapt("com.google.dagger:hilt-android-compiler:2.51.1") // kapt 用于注解处理器 // 确保 work-runtime-ktx 是 2.7.0 或更高版本 implementation("androidx.work:work-runtime-ktx:2.9.0") // 或者更新到最新稳定版 // 如果您使用 ViewModel,需要添加 Hilt 的 ViewModel 依赖 implementation("androidx.hilt:hilt-navigation-compose:1.2.0") // 如果是Compose implementation("androidx.hilt:hilt-work:1.2.0") // 如果是WorkManager kapt("androidx.hilt:hilt-compiler:1.2.0") // 对应 Hilt 的 ViewModel/WorkManager 编译器//省略.....
}
注意:
com.google.dagger.hilt.android
和com.google.dagger:hilt-android-compiler
的版本号需要保持一致。kotlin("kapt")
插件必须添加,因为 Hilt 使用注解处理器在编译时生成代码。
步骤 2:启用 Hilt 的 Application 类
您的应用程序必须有一个 Application 类,并使用 @HiltAndroidApp
注解对其进行标注。这将触发 Hilt 代码的生成,并作为应用程序级别的依赖容器。
package com.wy.demo import android.app.Application
import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp
class MyApplication : Application() { // 你可以在这里进行一些全局的初始化操作
}
然后,您需要在 AndroidManifest.xml
中指定这个 Application 类:
步骤 3:修改UserRepositoryImpl构造函数
Hilt 会通过 @Inject 构造函数来创建这个类的实例。Hilt 知道如何提供 Android 的 Context
。它会根据您将 UserRepositoryImpl
所属的 Module 安装到的 Hilt 组件,提供相应作用域的 Context
当您在类的构造函数上添加 @Inject
注解时,您就告诉 Hilt/Dagger:
- “当有人需要 这个类的实例时,请使用这个构造函数来创建它。”
- “这个构造函数中声明的所有参数,都是
MyClass
所依赖的其他对象。请 Hilt/Dagger 帮我查找并提供这些依赖的实例。”
@ApplicationContext
的主要作用是告诉 Hilt:
- 当您在构造函数或字段中请求
Context
时,您需要的是应用程序级别的Context
。 - **Hilt 会自动提供
Application
类的实例作为这个 `Context
步骤 4:创建 Hilt Module 来提供 Repository 的实例
由于 UserRepository
是一个接口,Hilt 无法直接通过构造函数注入来知道应该提供哪个实现类。因此,我们需要一个 Hilt Module 来告诉它。
// 创建 Hilt Module (例如: RepositoryModule.kt)
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton@Module // 标记这是一个 Hilt Module
@InstallIn(SingletonComponent::class) // 告诉 Hilt 这个 Module 应该安装到哪个组件中,这里是 Application 级别的单例组件
abstract class RepositoryModule { // 如果 Module 只包含抽象的 @Binds 方法,它可以是抽象类// 使用 @Binds 注解告诉 Hilt:// 当有人请求 UserRepository 接口时,请提供 UserRepositoryImpl 的实例。// 同时,使用 @Singleton 确保整个应用生命周期内 UserRepositoryImpl 只有一个实例。@Singleton@Bindsabstract fun bindUserRepository(userRepositoryImpl: UserRepositoryImpl // Hilt 会自动注入 UserRepositoryImpl 的实例): UserRepository
}
可以在同一个 RepositoryModule
中定义多个 @Binds
或 @Provides
方法,用于绑定不同的接口或者提供不同的依赖项。假设你现在需要增加一个绑定:
ProductRepository
和ProductRepositoryImpl
增加一个方法
// 产品 Repository 绑定
@Singleton
@Binds
abstract fun bindProductRepository(productRepositoryImpl: ProductRepositoryImpl
): ProductRepository
RepositoryModule.kt
文件放在
步骤 5: ViewModel 中注入 Repository
在 ViewModel 的构造函数中直接注入 UserRepository
接口。Hilt 会根据 RepositoryModule
中定义的绑定规则,提供 UserRepositoryImpl
的实例。修改DemoViewModel.kt
@HiltViewModel
是 Hilt 提供的一个注解,专门用于 ViewModel 的依赖注入。它的作用是将 Hilt 的依赖注入机制 与 Jetpack 的 ViewModel 结合起来,使得 ViewModel
可以方便地获取所需的依赖(如 Repository、DataSource 等)但具体是怎么做到的,没有了解,知道有这个就行。
步骤 6: Composable/ Activity中注入 Repository
Composable 中使用 ViewModel
hiltViewModel()
与 Hilt 的依赖注入机制深度集成。它会自动解析 @HiltViewModel
标记的 ViewModel
的依赖(如 Repository、DataSource 等),并确保依赖注入正确执行。
如果 ViewModel
没有使用 @HiltViewModel
,hiltViewModel()
仍然可以工作(但依赖注入不会生效)。
Activity 中使用 ViewModel
DemoActivity
中使用了需要依赖注入的 ViewModel
(即 ViewModel
标记了 @HiltViewModel
),那么 DemoActivity
必须添加 @AndroidEntryPoint
。这是 Hilt 的强制要求,
原因如下
@AndroidEntryPoint
是 Hilt 的标记注解,告诉 Hilt:“这个 Activity
需要依赖注入”。如果没有 @AndroidEntryPoint
,Hilt 不会为 Activity
生成依赖注入代码,导致 hiltViewModel()
无法工作。
最终达到的效果
在UI层面并没有变化,只是退出APP后,输入的名字被保存了,再次打开APP的时候,会在DemoViewModel初始化的时候读取已经存储的名字作为默认值