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 中使用的型別會使 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. 應用程式會收集這些變更並更新 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) }
)

提供意見

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

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

其他資源

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

範例

網誌

程式碼研究室