当前位置: 首页 > news >正文

安卓学习笔记-数据存储

阅读说明

本文是基于上一篇文章《安卓学习笔记-声明式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中的DaoService

用一个完整示例说明:用 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 的方式,用于存储键值对或结构化数据,具有更高的安全性和性能。

分为两种:

  1. Preferences DataStore 键值对存储(像 SharedPreferences
  2. 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.androidcom.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:

  1. 当您在构造函数或字段中请求 Context 时,您需要的是应用程序级别的 Context
  2. **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 方法,用于绑定不同的接口或者提供不同的依赖项。假设你现在需要增加一个绑定:

  • ProductRepositoryProductRepositoryImpl
    增加一个方法
// 产品 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 没有使用 @HiltViewModelhiltViewModel() 仍然可以工作(但依赖注入不会生效)。

Activity 中使用 ViewModel

DemoActivity 中使用了需要依赖注入的 ViewModel(即 ViewModel 标记了 @HiltViewModel),那么 DemoActivity 必须添加 @AndroidEntryPoint​。这是 Hilt 的强制要求,

原因如下
@AndroidEntryPoint 是 Hilt 的标记注解,告诉 Hilt:“这个 Activity 需要依赖注入”。如果没有 @AndroidEntryPoint,Hilt 不会为 Activity 生成依赖注入代码,导致 hiltViewModel() 无法工作。
在这里插入图片描述

最终达到的效果

在UI层面并没有变化,只是退出APP后,输入的名字被保存了,再次打开APP的时候,会在DemoViewModel初始化的时候读取已经存储的名字作为默认值
在这里插入图片描述

相关文章:

  • 为 Ubuntu 安装的软件创建桌面图标
  • 电路图识图基础知识-电路接线图(八)
  • Linux程序管理练习题
  • Python完整项目结构的示例及其说明
  • How to Initiate Back-to-Back Write Transactions from Master
  • RockyLinux9安装Docker
  • AI绘画提示词:从零开始掌握Prompt Engineering的艺术
  • 【电路笔记 TMS320F28335DSP】McBSP 从源时钟得到 生成时钟 CLKG 帧同步信号 FSG
  • 设计模式-工厂方法模式
  • Git的三种合并方式
  • LeetCode 395.至少有K个重复字符的最长子串
  • Git 全平台安装指南:从 Linux 到 Windows 的详细教程
  • 2025年机械化设计制造与计算机工程国际会议(MDMCE 2025)
  • 【循环神经网络RNN第一期】循环神经网络RNN原理概述
  • 【LeetCode 热题 100】最小路径和 / 最长回文子串 / 最长公共子序列 / 编辑距离
  • TMS320F28388D使用sysconfig配置IPC
  • GJOI 5.27 题解
  • IPTV电视直播 1.6.0 | 手机电视直播 秒播无卡顿
  • 降低实验检测报告编制耗时 质检LIMS系统的应用策略
  • 结构体对齐和结构体相关宏
  • 如何做话费卡回收网站/我要登录百度
  • 游戏推广代理/江苏搜索引擎优化
  • matlab做网站爬虫/太原seo外包平台
  • 怎样自己做商场网站/怎么提升关键词的质量度
  • 做那种事免费网站/免费个人网站建站申请
  • 枣庄市政府采购网/多合一seo插件破解版