Jetpack架构组件库:DataStore

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack架构组件库:DataStore相关的知识,希望对你有一定的参考价值。

Jetpack DataStore 是一种经过改进的新数据存储解决方案,旨在取代 SharedPreferences。DataStore 基于 Kotlin 协程和 Flow 构建而成,提供以下两种不同的实现:

  • Preferences DataStore 用于键值对存储。数据以异步、一致和事务性的方式存储,有助于避免 SharedPreferences 的一些缺点。此实现不需要预定义的架构,也不确保类型安全。
  • Proto DataStore 用于存储类型化对象,数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。 与 XML 和其他类似的数据格式相比,协议缓冲区速度更快、规格更小、使用更简单,并且更清楚明了。
功能SharedPreferencesPreferencesDataStoreProtoDataStore
异步 API✅(仅用于通过监听器读取已更改的值)✅(通过 Flow 以及 RxJava 2 和 3 Flowable)✅(通过 Flow 以及 RxJava 2 和 3 Flowable)
同步 API✅(但无法在界面线程上安全调用)
可在界面线程上安全调用✅(这项工作已在后台移至 Dispatchers.IO)✅(这项工作已在后台移至 Dispatchers.IO)
可以提示错误
不受运行时异常影响
包含一个具有强一致性保证的事务性 API
处理数据迁移
类型安全✅ 使用协议缓冲区

SharedPreferences的缺陷:

  • SharedPreferences 有一个看上去可以在界面线程中安全调用的同步 API,但是该 API 实际上执行磁盘 I/O 操作。此外,apply() 会阻断 fsync() 上的界面线程。每次有服务启动或停止以及每次 activity 在应用中的任何地方启动或停止时,系统都会触发待处理的 fsync() 调用。界面线程在 apply() 调度的待处理 fsync() 调用上会被阻断,这通常会导致 ANR

  • SharedPreferences 还会将解析错误作为运行时异常抛出。

如果您当前在使用 SharedPreferences 存储数据,请考虑迁移到 DataStore

注意:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 Room,而不是 DataStore。DataStore 的目的是存储简单的小型数据集, 但不支持部分更新或引用完整性。

为了正确使用 DataStore,请始终谨记以下规则:

  1. 请勿在同一进程中为给定文件创建多个 DataStore 实例,否则会破坏所有 DataStore
    功能。如果给定文件在同一进程中有多个有效的 DataStoreDataStore 在读取或更新数据时将抛出
    IllegalStateException

  2. DataStore 的通用类型必须不可变。更改 DataStore 中使用的类型会导致 DataStore 提供的所有保证失效,并且可能会造成严重的、难以发现的 bug。强烈建议您使用可保证不可变性、具有简单的 API
    且能够高效进行序列化的协议缓冲区。

  3. 切勿在同一个文件中混用 SingleProcessDataStore 和 MultiProcessDataStore。如果您打算从多个进程访问 DataStore,请始终使用 MultiProcessDataStore

Preferences DataStore 的使用

Preference DataStore API 类似于 SharedPreferences,但与后者相比存在一些显著差异:

  • 以事务方式处理数据更新
  • 公开表示当前数据状态的 Flow
  • 不提供存留数据的方法(apply()、commit())
  • 不返回对其内部状态的可变引用
  • 通过类型化键提供类似于 Map 和 MutableMap 的 API

添加依赖:

dependencies 
    implementation("androidx.datastore:datastore-preferences:1.0.0") 

Preferences DataStore 实现使用 DataStore 和 Preferences 类将简单的键值对保留在磁盘上。

创建 Preferences DataStore

使用由 preferencesDataStore 提供的属性委托来创建 Datastore<Preferences> 实例。只需在 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 DataStore 保留为单例。

private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME
)

从 Preferences DataStore 读取数据

由于 Preferences DataStore 不使用预定义的架构,因此必须使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey()。然后,使用 DataStore.data 属性,通过 Flow 提供适当的存储值。

private object PreferencesKeys 
     val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
     val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
 

 val counterFlow: Flow<Int> = context.dataStore.data.map  preferences ->
         preferences[EXAMPLE_COUNTER] ?: 0
 
	
 val completeFlow: Flow<Boolean> = context.dataStore.data.map  preferences ->
        preferences[SHOW_COMPLETED] ?: false
	

如果要读取的内容很多,可以定义一个data class来存储,在dataStore.data.map中返回该数据类对象即可:

data class UserPreferences(val count: Int, val show: Boolean)

val userPreferenceFlow = context.dataStore.data.map  preferences ->
    val count = preferences[EXAMPLE_COUNTER] ?: 0
    val show = preferences[SHOW_COMPLETED] ?: false
    UserPreferences(count, show)

处理读取数据时的异常

当 DataStore 从文件读取数据时,如果读取数据期间出现错误,系统会抛出 IOExceptions。我们可以通过以下方式处理这些事务:在 map() 之前使用 catch() Flow 运算符,并且在抛出的异常是 IOException 时发出 emptyPreferences()。如果出现其他类型的异常,最好重新抛出该异常。

val userPreferenceFlow = context.dataStore.data
    .catch  exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) 
            emit(emptyPreferences())
         else 
            throw exception
        
    .map  preferences ->
        val count = preferences[EXAMPLE_COUNTER] ?: 0
        val show = preferences[SHOW_COMPLETED] ?: false
        UserPreferences(count, show)
    

也可以选择在外面包裹一层 try-catch 进行处理。

将数据写入 Preferences DataStore

Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。

 suspend fun updateShowCompleted(showCompleted: Boolean) 
     try 
         context.dataStore.edit  preferences ->
             val currentCounterValue = preferences[EXAMPLE_COUNTER] ?: 0
             preferences[EXAMPLE_COUNTER] = currentCounterValue + 1
             preferences[SHOW_COMPLETED] = showCompleted
         
      catch (e: IOException) 
         println(e)
     
 

如果在读取或写入磁盘时发生错误,edit() 可能会抛出 IOException。如果转换块中出现任何其他错误,edit() 将抛出异常。

结合 ViewModel 和 Compose 使用的完整示例

下面是一个在 Compose 中使用包含 ViewModel 、Repository 和 DataStore 的完整示例:

// DataStore.kt
private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME
)

private object PreferencesKeys 
    val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
    val EXAMPLE_COUNTER = intPreferencesKey("example_counter")


data class UserPreferences(val count: Int, val show: Boolean)

class UserPreferencesRepository(val context: Context)   

    val userPreferenceFlow = context.dataStore.data
        .catch  exception ->
            // dataStore.data throws an IOException when an error is encountered when reading data
            if (exception is IOException) 
                emit(emptyPreferences())
             else 
                throw exception
            
        .map  preferences ->
            val count = preferences[EXAMPLE_COUNTER] ?: 0
            val show = preferences[SHOW_COMPLETED] ?: false
            UserPreferences(count, show)
         

    suspend fun updateShowCompleted(showCompleted: Boolean) 
        try 
            context.dataStore.edit  preferences ->
                val currentCounterValue = preferences[EXAMPLE_COUNTER] ?: 0
                preferences[EXAMPLE_COUNTER] = currentCounterValue + 1
                preferences[SHOW_COMPLETED] = showCompleted
            
         catch (e: IOException) 
            println(e)
        
    


class DataStoreViewModel(private val repository: UserPreferencesRepository): ViewModel() 

    val userPreference = repository.userPreferenceFlow

    fun updateShowCompleted(showCompleted: Boolean) 
        viewModelScope.launch  repository.updateShowCompleted(showCompleted) 
    

    // Define ViewModel factory in a companion object
    companion object 
        val Factory: ViewModelProvider.Factory = viewModelFactory 
            initializer 
                val repository = (this[APPLICATION_KEY] as MyApp).userPreferencesRepository
                DataStoreViewModel(repository)
            
        
    

// MyApp.kt
class MyApp: Application() 
    val userPreferencesRepository by lazy  UserPreferencesRepository(this)

// DataStoreActivity.kt
class DataStoreActivity: ComponentActivity() 

    val viewModel by viewModels<DataStoreViewModel>  DataStoreViewModel.Factory 

    @OptIn(ExperimentalLifecycleComposeApi::class)
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContent 
            MyComposeApplicationTheme 
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) 
                    val userPreferences by viewModel.userPreference.collectAsStateWithLifecycle(UserPreferences(0, false))
                    Column 
                        Text("$userPreferences")
                        Button(onClick = 
                            viewModel.updateShowCompleted(true)
                        ) 
                            Text("更新userPreferences")
                        
                    
                
            
        
    

从 SharedPreferences 迁移到 Preferences DataStore

为了能够将SharedPreferences迁移到 DataStore,我们需要更新 DataStore 构建器以向迁移列表传入 SharedPreferencesMigrationDataStore 能够自动从 SharedPreferences 迁移到 DataStore。迁移需在 DataStore 中的任何数据访问操作可发生之前运行。这意味着,必须在 DataStore.data 发出任何值之前和 DataStore.edit() 可以更新数据之前,成功完成迁移。

private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    produceMigrations =  context ->
        // Since we're migrating from SharedPreferences, add a migration based on the SharedPreferences name
        listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    
)

注意:由于键只能从 SharedPreferences 迁移一次,因此在代码迁移到 DataStore 之后,您应停止使用旧 SharedPreferences

Proto DataStore 的使用

Proto DataStore 实现使用 DataStore 和协议缓冲区将类型化的对象保留在磁盘上。

SharedPreferencesPreferences DataStore 的一个缺点是无法定义架构,保证不了存取键时使用了正确的数据类型。Proto DataStore 可利用协议缓冲区定义架构来解决此问题。通过使用协议,DataStore 可以知道存储的类型,并且无需使用键便能提供类型。

接下来,我们看看如何将 Proto DataStore 和协议缓冲区添加到项目中中。

添加依赖项:

plugins 
    ...
    id "com.google.protobuf" version "0.8.18"


dependencies 
    implementation  "androidx.datastore:datastore:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.21.12"
    implementation  "com.google.protobuf:protobuf-kotlin-lite:3.21.12"
    ...


protobuf 
    protoc 
        artifact = "com.google.protobuf:protoc:3.21.12"
    

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks 
        all().each  task ->
            task.builtins 
                java 
                    option 'lite'
                
                kotlin 
            
        
    

协议缓冲区是一种对结构化数据进行序列化的机制。您只需对数据结构化的方式进行一次定义,编译器便会生成源代码,轻松写入和读取结构化数据。

创建 proto 文件

Proto DataStore 要求在 app/src/main/proto/ 目录的 proto 文件中保存预定义的架构。此架构用于定义您在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅 protobuf 语言指南

app/src/main/proto 目录中创建一个名为 user_prefs.proto 的新文件。添加内容如下:

syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences  
  bool show_completed = 1;
  int32 example_counter = 2;
  string name = 3;

注意:UserPreferences 类在编译时会从 proto 文件中定义的 message 中生成。请务必重新构建该项目。

创建 Proto DataStore

创建 Proto DataStore 来存储类型化对象涉及两个步骤:

  1. 定义一个实现 Serializer<T> 的类,其中 Tproto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。
  2. 使用由 dataStore 创建的属性委托来创建 DataStore<T> 的实例,其中 Tproto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename 参数会告知 DataStore 使用哪个文件存储数据,而 serializer 参数会告知 DataStore1 步中定义的序列化器类的名称。
创建序列化器

如需告知 DataStore 如何读取和写入我们在 proto 文件中定义的数据类型,我们需要实现序列化器。如果磁盘上没有数据,序列化器还会定义默认返回值。

创建一个名为 UserPreferencesSerializer 的新文件:

object UserPreferencesSerializer : Serializer<UserPreferences> 
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    override suspend fun readFrom(input: InputStream): UserPreferences 
        try 
            return UserPreferences.parseFrom(input)
         catch (exception: InvalidProtocolBufferException) 
            throw CorruptionException("Cannot read proto.", exception)
        
    

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)

提示:如果发现找不到 UserPreferences 对象或相关方法,请在项目目录右键选择 Reload from Disk , 若仍未找到,请执行 Clean and Rebuild,以确保协议缓冲区生成对象。

创建 DataStore

为了创建 DataStore 实例,我们使用 dataStore 委托,并将 Context 作为接收器。此委托有两个必需参数:

  • DataStore 会处理的文件的名称。
  • DataStore 使用的类型的序列化器。我们使用前面定义的序列化器:UserPreferencesSerializer
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"

private val Context.userDataStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer
)

从 Proto DataStore 中读取数据

val exampleCounterFlow: Flow<Int> = context.userDataStore.data.map  preferences ->
        // The exampleCounter property is generated from the proto schema.
        preferences.exampleCounter


val userPreferencesFlow: Flow<UserPreferences> = context.userDataStore.data
读取数据时处理异常

由于 DataStore 从文件中读取数据,因此如果读取数据时出现错误,系统会抛出 IOException。我们可以使用 catch Flow 转换来处理这些异常,只需记录错误即可:

val userPreferencesFlow: Flow<UserPreferences> = context.userDataStore.data
    .catch  exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) 
            Log.e(TAG, "Error reading sort order preferences.", exception)
            emit(UserPreferences.getDefaultInstance())
         else 
            throw exception
        
    

将数据写入 Proto DataStore

Proto DataStore 提供了一个挂起函数 updateData() ,用于以事务方式更新存储的对象。updateData() 在读取-写入-修改原子操作中用事务的方式更新数据。一旦数据持久存储在磁盘中,协程便会完成。

suspend fun updateShowCompleted(completed: Boolean, name: String) 
     context.userDataStore.updateData  preferences ->
         preferences.toBuilder()
             .setShowCompleted(completed)
             .setExampleCounter(preferences.exampleCounter + 1)
             .setName(name)
             .build()
     
 

在同步代码中使用 DataStore

DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。

Kotlin 协程提供 runBlocking() 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 runBlocking() 从 DataStore 同步读取数据。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据:

val exampleData = runBlocking  context.dataStore.data.first() 

对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题:

override fun onCreate(savedInstanceState: Bundle?) 
    lifecycleScope.launch 
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    

这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 runBlocking() 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。

以 Json 序列化的方式使用 Proto DataStore

下面提供一种通过 Json 序列化的方式来使用 Proto DataStore ,可以不用创建proto文件,依然可以保证类型安全。

添加依赖:

plugins 
	id 'org.jetbrains.kotlin.plugin.serialization'
	...

dependencies 
	implementation  "androidx.datastore:datastore:1.0.0" 
	implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"

// 根目录下添加
plugins 
	id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.21'  apply false

定义Serializable的数据类:

@Serializable
data class UserInfo(
    val name: String = "未知",
    val age: Int = 0,
    val sex: Sex = Sex.MALE,
    val postList: List<PostInfo> = listOf()
)

@Serializable
data class PostInfo(val title: String, val time: Long)

enum class Sex  MALE, FEMALE 

定义 UserInfoSerializer 序列化器:

object UserInfoSerializer: Serializer<UserInfo> 
    override suspend fun readFrom(input: InputStream): UserInfo 
        return try 
            Json.decodeFromString(
                deserializer = UserInfo.serializer(),
                string = input.readBytes().decodeToString()
            )
         catch (e: SerializationException) 
            e.printStackTrace()
            defaultValue
        
    

    override suspend fun writeTo(t: UserInfo, output: OutputStream) 
        output.write(
            Json.encodeToString(
                serializer = UserInfo.serializer(),
                value = t
            ).encodeToByteArray()
        )
    

    override val defaultValue: UserInfo
        get() = UserInfo()

这里 UserInfoSerializer 使用Json.encodeToString()Json.decodeFromString() 进行序列化和反序列化。

定义 dataStore :

val Context.userInfoDataStore by dataStore("app-settings.json", UserInfoSerializer)

定义UserInfoViewModel

class UserInfoViewModel(val app : Application): ViewModel() 

    val userInfo: Flow<UserInfo> = app.userInfoDataStore.data

    fun updateUserInfo(user: UserInfo) 
        viewModelScope.launch 
            app.userInfoDataStore.updateData 
                user
            
        
    

    fun addUserPostInfo(postInfo: PostInfo) 
        viewModelScopeJetpack组件库(含Jetpack Compose)从入门到精通全家桶附Demo

Android Jetpack组件 DataStore的使用和简单封装

Android Jetpack组件 DataStore的使用和简单封装

Jetpack DataStore 你总要了解一下吧?

Jetpack DataStore 你总要了解一下吧?

Jetpack DataStore 你总要了解一下吧?