安卓学习笔记-数据存储

article/2025/9/7 17:52:51

阅读说明

本文是基于上一篇文章《安卓学习笔记-声明式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初始化的时候读取已经存储的名字作为默认值
在这里插入图片描述


http://www.hkcw.cn/article/yNZvADfgwA.shtml

相关文章

cutlass学习教程

一 接口 1.1 内存类 1.1.1 DeviceAllocation 1 位置 2 内置函数 &#xff08;a&#xff09;reset /// Deletes the managed object and resets capacity to zero void reset() {capacity 0;smart_ptr.reset(); } &#xff08;b&#xff09;get /// Returns a pointer to t…

自定义异常小练习

在开始之前,让我们高喊我们的口号&#xff1a; ​​​​​​​ 键盘敲烂,年薪百万&#xff01; 目录 键盘敲烂,年薪百万&#xff01; 异常综合练习&#xff1a; 自定义异常 异常综合练习&#xff1a; 自定义异常&#xff1a; 定义异常类写继承关系空参构造带参构造 自定…

计算机网络

OSI七层模型 应用层&#xff1a;直接为用户提供网络服务&#xff0c;例如网页浏览、邮件收发表示层&#xff1a;处理数据格式&#xff0c;如加密、解密、压缩、编码等会话层&#xff1a;利用传输层提供的服务&#xff0c;在应用程序之间建立和维持会话&#xff0c;并能使会话获…

【循环神经网络RNN第一期】循环神经网络RNN原理概述

目录 &#x1f9e0; 什么是循环神经网络&#xff08;RNN&#xff09;&#xff1f;&#x1f501; RNN 的结构图&#x1f504; RNN 的“记忆”与问题RNN梯度推导 &#x1f9ec; LSTM&#xff1a;解决长期依赖问题&#x1f9f1; LSTM 的核心结构LSTM总结 参考 人类在思考的时候&am…

自动驾驶与智能交通:构建未来出行的智能引擎

随着人工智能、物联网、5G和大数据等前沿技术的发展&#xff0c;自动驾驶汽车和智能交通系统正以前所未有的速度改变人类的出行方式。这一变革不仅是技术的融合创新&#xff0c;更是推动城市可持续发展的关键支撑。 一、自动驾驶与智能交通的定义 1. 自动驾驶&#xff08;Auto…

5.3.1_2二叉树的层次遍历

遍历过程&#xff1a; 从根节点开始&#xff0c;从左到右一层一层遍历&#xff0c;如下&#xff1a;ABCDEFGHIJKL 初始化一个辅助队列&#xff0c;让根节点先入队&#xff0c;每次判断队列是否为空&#xff0c;不空则让队头节点出队访问该节点让该节点左右孩子入队尾(先左孩子…

Qt DateTimeEdit(时间⽇期的微调框)

使⽤ QDateEdit 作为⽇期的微调框. 使⽤ QTimeEdit 作为时间的微调框 使⽤ QDateTimeEdit 作为时间⽇期的微调框. 这⼏个控件⽤法⾮常相似, 我们以 QDateTimeEdit 为例进⾏介绍. QDateTimeEdit 核⼼属性 属性说明dateTime时间⽇期的值. 形如 2000/1/1 0:00:00date单纯⽇期…

NISCO里境全新VALUE系列合肥首店启幕,携手正反设计打造0压生活空间

2025年5月28日,NISCO里境全新VALUE系列全国首家旗舰店正式落子合肥,梦百合集团近三十位核心经销商代表齐聚,深度体验新系列门店的空间场景与创新产品矩阵。 作为品牌“双线并进”的重要布局,VALUE系列以高性价比为核心,聚焦二三线城市消费需求,通过沉浸式空间场景与革新性产品设…

制造企业搭建AI智能生产线怎么部署?

制造商需要精准协调生产和发货&#xff0c;确保订单及时交付。MES、ERP、CRM 系统与生产线集成&#xff0c;对生产管理流程、物料跟踪、品控、确定货期至关重要。如果某个系统发生延迟或者效率低下&#xff0c;会在造成整个生产环节停滞&#xff0c;影响最终交付&#xff0c;导…

Linux系统管理与编程24:基础条件准备-混搭“本地+阿里云”yum源

兰生幽谷&#xff0c;不为莫服而不芳&#xff1b; 君子行义&#xff0c;不为莫知而止休。 1.添加宿主机共享文件夹 Linux虚拟机可以和宿主机共享文件夹&#xff0c;这样有利于工具文件的共享。具体操作如下&#xff1a; 1&#xff09;vmware workstation共享文件夹 虚拟机…

VCS elab选项 -simprofile功能

#废话不多说&#xff0c;直接上干货 1.简介 VCS提供的simprofile功能是用于分析仿真过程中的CPU time和machine memory消耗情况&#xff0c;可以协助定位一些垃圾代码写法导致的仿真资源消耗过大问题&#xff1b;本篇内容包含&#xff0c;应用此功能的必要工具准备、makefile…

30万数据 动态查出用户拥有对应 skuid 数量

项目场景&#xff1a; 提示&#xff1a;30万数据 动态查出用户拥有对应 skuid 数量&#xff1a; 如 skuid1 skuid2 skuid3 skuid4 … 表结构如下 CREATE TABLE eb_nft_user (id int(10) unsigned NOT NULL AUTO_INCREMENT,skuId varchar(255) NOT NULL DEFAULT COMMENT 商品…

快递物流查询接口如何用C#进行调用?

一、什么是快递物流查询接口 支持国内外1500快递公司跟踪服务&#xff0c;包括顺丰、圆通、韵达等主流快递公司。快递物流查询接口为连接电商平台、物流企业与终端用户之间的桥梁&#xff0c;正在发挥着不可替代的作用。它不仅帮助用户实时掌握包裹动态&#xff0c;也为物流企…

【C语言】函数指针及其应用

目录 1.1 函数指针的概念和应用 1.2 赋值与内存模型 1.3 调用方式与注意事项 二、函数指针的使用 2.1 函数指针的定义和访问 2.2 动态调度&#xff1a;用户输入驱动函数执行 2.3 函数指针数组进阶应用 2.4 函数作为参数的高阶抽象 三、回调函数 3.1 指针函数…

第十三章:预处理

预处理功能是C语言特有的功能,可以使用预处理和具有预处理的功能是C 语言和其他高级语言的区别之一。预处理程序包含许多有用的功能,如宏定义、条件编译等,使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。 通过本章的学习,您可以: 1、掌握…

9.4 Q1|复旦大学CHARLS发文 | 老年人肌肉减少症和轻度认知障碍

1.第一段-文章基本信息 文章题目&#xff1a;Sarcopenia and mild cognitive impairment among elderly adults: The first longitudinal evidence from CHARLS 中文标题&#xff1a;老年人肌肉减少症和轻度认知障碍&#xff1a;来自CHARLS 的第一个纵向证据 发表杂志&#x…

python h5py 读取mat文件的<HDF5 object reference> 问题

我用python加载matlab的mat文件 mat文件&#xff1a; 加载方式&#xff1a; mat_file h5py.File(base_dir str(N) _nodes_dataset_snr- str(snr) _M_ str(M) .mat, r) Signals mat_file["Signals"][()] Tp mat_file["Tp"][()] Tp_list mat_fil…

【数据结构】图论核心算法解析:深度优先搜索(DFS)的纵深遍历与生成树实战指南​

深度优先搜索 导读&#xff1a;从广度到深度&#xff0c;探索图的遍历奥秘一、深度优先搜索二、算法思路三、算法逻辑四、算法评价五、深度优先生成树六、有向图与无向图结语&#xff1a;深潜与回溯&#xff0c;揭开图论世界的另一面 导读&#xff1a;从广度到深度&#xff0c;…

扫地机产品异物进入吸尘口堵塞异常检测方案

扫地机产品异物进入吸尘口堵塞异常的检测方案 文章目录 扫地机产品异物进入吸尘口堵塞异常的检测方案一.背景二.石头的音频异常检测的方案2.1 音频检测触发点2.1.1时间周期2.1.2根据清洁机器人清扫模式或清扫区域污渍类型,即当清扫模式为深度清洁模式 或清扫区域污渍类型为重度…

【Mini-F5265-OB开发板试用测评】RT-Thread的移植(调通串口+LED)

前言 近期&#xff0c;笔者有幸获得灵动微MM32F5265开发板的体验资格。MM32F5260搭载了arm china "Star-MC1 内核"处理器&#xff0c;拥有高性能&#xff0c;主打家电和工业等高可靠性应用领域。 总结一下&#xff0c;有以下亮点&#xff1a; 本土团队打造&#x…