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.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(...)

提供反馈

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

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

其他资源

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

示例

博客

Codelabs