DataStore   属于 Android Jetpack

Jetpack DataStore 是一种数据存储解决方案,允许您使用Protocol Buffers存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 来异步、一致且事务性地存储数据。

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

Preferences DataStore 和 Proto DataStore

DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。

  • Preferences DataStore 使用键存储和访问数据。此实现不需要预定义的模式,也不提供类型安全。
  • Proto DataStore 将数据存储为自定义数据类型的实例。此实现要求您使用Protocol Buffers定义模式,但它提供类型安全。

正确使用DataStore

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

  1. 切勿为同一进程中的给定文件创建多个DataStore实例。这样做会破坏所有 DataStore 功能。如果同一进程中给定文件的多个 DataStore 处于活动状态,则 DataStore 在读取或更新数据时将抛出IllegalStateException

  2. DataStore 的泛型类型必须是不可变的。更改 DataStore 中使用的类型会使 DataStore 提供的任何保证无效,并可能创建难以发现的严重错误。强烈建议您使用 Protocol Buffers,它提供不可变性保证、简单的 API 和高效的序列化。

  3. 切勿将SingleProcessDataStoreMultiProcessDataStore混合用于同一文件。如果您打算从多个进程访问DataStore,请始终使用MultiProcessDataStore

设置

要在您的应用中使用 Jetpack DataStore,请根据您要使用的实现将以下内容添加到您的 Gradle 文件中

Preferences DataStore

Groovy

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.1"
    }
    

Kotlin

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.1")
    }
    

Proto DataStore

Groovy

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.1"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.1"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.1"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.1"
    }
    

Kotlin

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.1")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.1")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.1")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.1")
    }
    

使用 Preferences DataStore 存储键值对

Preferences DataStore 实现使用DataStorePreferences类将简单的键值对持久化到磁盘。

创建 Preferences DataStore

使用preferencesDataStore创建的属性委托来创建DataStore<Preferences>的实例。在 Kotlin 文件的顶层调用它一次,并在应用程序的其余部分通过此属性访问它。这使得更容易将您的DataStore保持为单例。或者,如果您使用的是 RxJava,请使用RxPreferenceDataStoreBuilder。必需的name参数是 Preferences DataStore 的名称。

Kotlin

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Java

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

从 Preferences DataStore 读取数据

由于 Preferences DataStore 不使用预定义的模式,您必须使用相应的键类型函数为需要存储在DataStore<Preferences>实例中的每个值定义一个键。例如,要为 int 值定义一个键,请使用intPreferencesKey()。然后,使用DataStore.data属性使用Flow公开相应存储的值。

Kotlin

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}

Java

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

写入 Preferences DataStore

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

Kotlin

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Java

Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
  MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
  Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
  return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

使用 Proto DataStore 存储类型化对象

Proto DataStore 实现使用 DataStore 和协议缓冲区将类型化对象持久化到磁盘。

定义模式

Proto DataStore 需要app/src/main/proto/目录中的 proto 文件中的预定义模式。此模式定义了在 Proto DataStore 中持久化对象的类型。要了解有关定义 proto 模式 的更多信息,请参阅protobuf 语言指南

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

创建 Proto DataStore

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

  1. 定义一个实现Serializer<T>的类,其中T是在 proto 文件中定义的类型。此序列化器类告诉 DataStore 如何读取和写入您的数据类型。确保为序列化器包含一个默认值,如果尚未创建文件,则使用该默认值。
  2. 使用dataStore创建的属性委托来创建DataStore<T>的实例,其中T是在 proto 文件中定义的类型。在 Kotlin 文件的顶层调用此方法一次,并在应用程序的其余部分通过此属性委托访问它。filename参数告诉 DataStore 使用哪个文件来存储数据,serializer参数告诉 DataStore 步骤 1 中定义的序列化器类的名称。

Kotlin

object SettingsSerializer : Serializer<Settings> {
  override val defaultValue: Settings = Settings.getDefaultInstance()

  override suspend fun readFrom(input: InputStream): Settings {
    try {
      return Settings.parseFrom(input)
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException("Cannot read proto.", exception)
    }
  }

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

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName = "settings.pb",
  serializer = SettingsSerializer
)

Java

private static class SettingsSerializer implements Serializer<Settings> {
  @Override
  public Settings getDefaultValue() {
    Settings.getDefaultInstance();
  }

  @Override
  public Settings readFrom(@NotNull InputStream input) {
    try {
      return Settings.parseFrom(input);
    } catch (exception: InvalidProtocolBufferException) {
      throw CorruptionException(Cannot read proto., exception);
    }
  }

  @Override
  public void writeTo(Settings t, @NotNull OutputStream output) {
    t.writeTo(output);
  }
}

RxDataStore<Byte> dataStore =
    new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();

从 Proto DataStore 读取数据

使用DataStore.data公开存储对象的相应属性的Flow

Kotlin

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Java

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

写入 Proto DataStore

Proto DataStore 提供了一个updateData()函数,该函数以事务方式更新存储的对象。updateData()为您提供数据的当前状态(作为数据类型的实例),并以原子读-写-修改操作以事务方式更新数据。

Kotlin

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Java

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

在同步代码中使用 DataStore

DataStore 的主要优点之一是异步 API,但更改周围代码以使其异步并不总是可行的。如果您正在处理使用同步磁盘 I/O 的现有代码库,或者您有一个不提供异步 API 的依赖项,则可能出现这种情况。

Kotlin 协程提供runBlocking()协程构建器来帮助弥合同步和异步代码之间的差距。您可以使用runBlocking()同步地从 DataStore 读取数据。RxJava 在Flowable上提供阻塞方法。以下代码会阻塞调用线程,直到 DataStore 返回数据。

Kotlin

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

Java

Settings settings = dataStore.data().blockingFirst();

在 UI 线程上执行同步 I/O 操作可能会导致 ANR 或 UI 卡顿。您可以通过异步预加载 DataStore 中的数据来减轻这些问题。

Kotlin

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

Java

dataStore.data().first().subscribe();

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

在多进程代码中使用 DataStore

您可以配置 DataStore 以访问不同进程中的相同数据,并具有与单进程中相同的 数据一致性保证。特别是,DataStore 保证:

  • 读取仅返回已持久化到磁盘的数据。
  • 读后写一致性。
  • 写入是串行化的。
  • 读取永远不会被写入阻塞。

考虑一个包含服务和活动的示例应用程序

  1. 该服务在单独的进程中运行并定期更新 DataStore

    <service
      android:name=".MyService"
      android:process=":my_process_id" />
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope.launch {
              while(isActive) {
                  dataStore.updateData {
                      Settings(lastUpdate = System.currentTimeMillis())
                  }
                  delay(1000)
              }
          }
    }
    
  2. 同时,应用程序将收集这些更改并更新其 UI

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text = "Last updated: $${settings.timestamp}",
    )
    

为了能够跨不同进程使用 DataStore,您需要使用MultiProcessDataStoreFactory构造 DataStore 对象。

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)

serializer告诉 DataStore 如何读取和写入您的数据类型。确保为序列化器包含一个默认值,如果尚未创建文件,则使用该默认值。以下是一个使用kotlinx.serialization的示例实现

@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Timer =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

您可以使用Hilt依赖注入来确保您的 DataStore 实例在每个进程中都是唯一的

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

处理文件损坏

在极少数情况下,DataStore 的持久化磁盘文件可能会损坏。默认情况下,DataStore 不会自动从损坏中恢复,尝试从中读取会导致系统抛出CorruptionException

DataStore 提供了一个损坏处理程序 API,可以帮助您在这种情况下优雅地恢复,并避免抛出异常。配置后,损坏处理程序会将损坏的文件替换为包含预定义默认值的新文件。

要设置此处理程序,请在by dataStore()中或在DataStoreFactory工厂方法中创建 DataStore 实例时提供corruptionHandler

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

提供反馈

通过以下资源与我们分享您的反馈和想法

问题跟踪器
报告问题,以便我们修复错误。

其他资源

要了解有关 Jetpack DataStore 的更多信息,请参阅以下其他资源

示例

博客

代码实验室