Datastore Android Jetpack 的一部分。

Jetpack DataStore 是一項資料儲存解決方案,可讓您使用通訊協定緩衝區儲存關鍵值組合或輸入的物件。DataStore 使用 Kotlin 處理 coroutines 和 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 提供和建立的所有保證會失效 可能出乎意料且難以捉摸的錯誤我們強烈建議您使用 通訊協定緩衝區,提供不變性保證、簡單的 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> 例項中的每個值定義金鑰。舉例來說,如要為整數值定義金鑰,請使用 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 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 coroutine 提供 runBlocking() coroutine 建構工具,協助消除同步與非同步程式碼之間的差距。您可以使用 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. 應用程式會收集這些變更並更新使用者介面

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

提供意見

歡迎透過下列資源與我們分享意見和想法:

Issue Tracker
報告問題,幫助我們修正錯誤。

其他資源

如要進一步瞭解 Jetpack DataStore,請參閱下列其他資源:

範例

網誌

程式碼研究室