DataStore   Android Jetpack 的一部分。

Jetpack DataStore 是一种数据存储解决方案,允许您使用 协议缓冲区 存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致和事务性方式存储数据。

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

Preferences DataStore 和 Proto DataStore

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

  • Preferences DataStore 使用键存储和访问数据。此实现不需要预定义的架构,也不提供类型安全。
  • Proto DataStore 将数据存储为自定义数据类型的实例。此实现要求您使用 协议缓冲区 定义架构,但它提供类型安全。

正确使用 DataStore

要正确使用 DataStore,请务必牢记以下规则

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

  2. DataStore 的泛型类型必须是不可变的。 更改 DataStore 中使用的类型会使 DataStore 提供的任何保证失效,并可能导致严重的、难以捕获的错误。强烈建议您使用协议缓冲区,它们提供不可变性保证、简单的 API 和高效的序列化。

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

设置

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

Preferences DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

使用 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.proto";
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();

在界面线程上执行同步 I/O 操作可能导致 ANR 或界面卡顿。您可以通过异步预加载 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 保证

  • 读取只返回已持久化到磁盘的数据。
  • 写后读一致性。
  • 写入操作是序列化的。
  • 读取操作永远不会被写入操作阻塞。

考虑一个包含服务和 Activity 的示例应用

  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. 同时应用会收集这些更改并更新其界面

    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): Settings =
       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 的更多信息,请参阅以下其他资源

示例

博客

Codelab