應用程式架構:資料層 - DataStore - Android 開發人員

Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: Explore this app architecture guide on data layer libraries to learn about Preferences DataStore and Proto DataStore, Setup, and more. hide_page_heading: true

DataStore   Android Jetpack 的一部分。

使用 Kotlin Multiplatform 試試看
Kotlin Multiplatform 可讓您與其他平台共用資料層。瞭解如何在 KMP 中設定及使用 DataStore

Jetpack DataStore 是一項資料儲存解決方案,可讓您使用通訊協定緩衝區儲存鍵/值組合或輸入的物件。DataStore 使用 Kotlin 處理 coroutines 和 Flow,以非同步、一致的方式和交易方式儲存資料。

如果您使用 SharedPreferences 儲存資料,請考慮改用 DataStore。

DataStore API

DataStore 介面提供下列 API:

  1. 可用於從 DataStore 讀取資料的 Flow

    val data: Flow<T>
    
  2. 更新 DataStore 中資料的函式

    suspend updateData(transform: suspend (t) -> T)
    

DataStore 設定

如要使用鍵儲存及存取資料,請使用 Preferences DataStore 實作,這項實作不需要預先定義的結構定義,且不提供型別安全。這個 API 類似於 SharedPreferences,但沒有共用偏好設定的缺點。

DataStore 可讓您保存自訂類別。如要執行這項操作,您必須定義資料的結構定義,並提供 Serializer 將資料轉換為可保存的格式。您可以選擇使用通訊協定緩衝區、JSON 或任何其他序列化策略。

設定

如要在應用程式中使用 Jetpack DataStore,請根據想採用的實作方式,將以下內容新增到 Gradle 檔案:

Preferences DataStore

在 Gradle 檔案的依附元件部分中新增下列幾行:

Groovy

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

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-preferences-core:1.1.7"
    }
    

Kotlin

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

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-preferences-core:1.1.7")
    }
    

如要新增選用的 RxJava 支援,請新增下列依附元件:

Groovy

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

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

Kotlin

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

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

DataStore

在 Gradle 檔案的依附元件部分中新增下列幾行:

Groovy

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation "androidx.datastore:datastore:1.1.7"

        // Alternatively - without an Android dependency.
        implementation "androidx.datastore:datastore-core:1.1.7"
    }
    

Kotlin

    dependencies {
        // Typed DataStore for custom data objects (for example, using Proto or JSON).
        implementation("androidx.datastore:datastore:1.1.7")

        // Alternatively - without an Android dependency.
        implementation("androidx.datastore:datastore-core:1.1.7")
    }
    

新增下列 RxJava 支援的選用依附元件:

Groovy

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

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

Kotlin

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

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

如要序列化內容,請新增通訊協定緩衝區或 JSON 序列化的依附元件。

JSON 序列化

如要使用 JSON 序列化,請在 Gradle 檔案中新增以下內容:

Groovy

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
    }
    

Kotlin

    plugins {
        id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20"
    }

    dependencies {
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
    }
    

Protobuf 序列化

如要使用 Protobuf 序列化,請在 Gradle 檔案中新增以下內容:

Groovy

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1"

    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

Kotlin

    plugins {
        id("com.google.protobuf") version "0.9.5"
    }
    dependencies {
        implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1")
    }

    protobuf {
        protoc {
            artifact = "com.google.protobuf:protoc:4.32.1"
        }
        generateProtoTasks {
            all().forEach { task ->
                task.builtins {
                    create("java") {
                        option("lite")
                    }
                    create("kotlin")
                }
            }
        }
    }
    

正確使用 DataStore

如要正確使用 DataStore,請務必遵守下列規則:

  1. 在同一個程序中,針對特定檔案建立 DataStore 的執行個體時,請勿建立超過一個。這麼做可能會導致所有 DataStore 功能無法運作。如果同一個程序中,某個檔案有多個有效的 DataStore,DataStore 會在讀取或更新資料時擲回 IllegalStateException

  2. DataStore<T> 的泛型型別必須不可變動。如果變更 DataStore 中使用的型別,DataStore 提供的資料一致性就會失效,而且可能會產生難以發現的嚴重錯誤。建議您使用通訊協定緩衝區,確保不變性、清楚的 API 和有效率的序列化。

  3. 請勿在同一個檔案中混用 SingleProcessDataStoreMultiProcessDataStore。如要從多個程序存取 DataStore,請使用 MultiProcessDataStore

資料定義

Preferences DataStore

定義要用於將資料保留在磁碟上的鍵。

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

如果是 JSON 資料儲存區,請將 @Serialization 註解新增至要保留的資料

@Serializable
data class Settings(
    val exampleCounter: Int
)

定義實作 Serializer<T> 的類別,其中 T 是您先前新增註解的類別類型。請確認您尚未建立序列化器要使用的預設值,如果您還尚未建立任何檔案的話。

object SettingsSerializer : Serializer<Settings> {

    override val defaultValue: Settings = Settings(exampleCounter = 0)

    override suspend fun readFrom(input: InputStream): Settings =
        try {
            Json.decodeFromString<Settings>(
                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(t)
                .encodeToByteArray()
        )
    }
}

Proto DataStore

Proto DataStore 實作會使用 DataStore 和通訊協定緩衝區,將輸入的物件保留到磁碟。

Proto DataStore 需要 app/src/main/proto/ 目錄中原始檔案的已預先定義的結構定義。這個結構定義定義了您儲存在 Proto DataStore 中的物件類型。如要進一步瞭解如何定義 Proto 結構定義,請參閱 「protobuf 語言指南」

src/main/proto 資料夾中新增名為 settings.proto 的檔案:

syntax = "proto3";

option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

定義實作 Serializer<T> 的類別,其中 T 是 Proto 檔案中定義的類型。這個序列化器類別會定義 DataStore 讀取及寫入資料類型的方式。請確認您尚未建立序列化器要使用的預設值,如果您還尚未建立任何檔案的話。

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) {
        return t.writeTo(output)
    }
}

建立 DataStore

您需要指定用於保存資料的檔案名稱。

Preferences DataStore

Preferences DataStore 實作會使用 DataStorePreferences 類別,將鍵/值組合保留在磁碟中。使用 preferencesDataStore 建立的屬性委派來建立 DataStore<Preferences> 的例項。請在 Kotlin 檔案的頂層呼叫一次。在應用程式的其他部分,透過此屬性存取 DataStore。這樣就能更輕鬆地將 DataStore 保持為單例模式。如果您使用 RxJava,請改用 RxPreferenceDataStoreBuilder。 必要的 name 參數是 Preferences DataStore 的名稱。

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

JSON DataStore

使用 dataStore 建立的屬性委派來建立 DataStore<T> 的例項,其中 T 是可序列化的資料類別。請先在 kotlin 檔案的頂層呼叫此方法,然後在應用程式的其餘部分透過此屬性存取該檔案。fileName 參數會告訴 DataStore 要使用哪個檔案儲存資料,而 serializer 參數會指示 DataStore 在步驟 1 中定義的序列化器的名稱。

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

Proto DataStore

使用 dataStore 建立的屬性委派來建立 DataStore<T> 的例項,其中 T 是 proto 檔案中所定義的類型。請先在 Kotlin 檔案的頂層呼叫此方法,然後在應用程式的其餘部分透過此屬性存取該檔案。fileName 參數會告訴 DataStore 要使用哪個檔案儲存資料,而 serializer 參數會指示 DataStore 在步驟 1 中定義的序列化器的名稱。

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

從 DataStore 讀取

您需要指定用於保存資料的檔案名稱。

Preferences DataStore

由於 Preferences DataStore 不使用預先定義的結構定義,因此您必須使用對應的鍵類型函式,為此您需要儲存在 DataStore<Preferences> 例項中的每個值定義鍵。舉例來說,如要為整數值定義鍵,請使用 intPreferencesKey()。然後使用 DataStore.data 屬性,並用 Flow 公開適當的儲存值。

fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
    preferences[EXAMPLE_COUNTER] ?: 0
}

JSON DataStore

使用 DataStore.data 即可從儲存的物件中顯示適當屬性的 Flow

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

Proto DataStore

使用 DataStore.data 即可從儲存的物件中顯示適當屬性的 Flow

fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
    settings.exampleCounter
}

寫入 DataStore

DataStore 提供 updateData() 函式,可交易式更新儲存的物件。updateData 會以資料類型的形式提供資料目前的狀態,並在不可部分完成的讀/寫/改作業中更新資料。updateData 區塊中的所有程式碼都視為單一交易。

Preferences DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData {
        it.toMutablePreferences().also { preferences ->
            preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
        }
    }
}

JSON DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy(exampleCounter = settings.exampleCounter + 1)
    }
}

Proto DataStore

suspend fun incrementCounter() {
    context.dataStore.updateData { settings ->
        settings.copy { exampleCounter = exampleCounter + 1 }
    }
}

Compose 範例

您可以將這些函式放在類別中,並在 Compose 應用程式中使用。

Preferences DataStore

現在我們可以將這些函式放入名為 PreferencesDataStore 的類別,並在 Compose 應用程式中使用。

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }

// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(
    onClick = {
        coroutineScope.launch { preferencesDataStore.incrementCounter() }
    }
) {
    Text("increment")
}

JSON DataStore

現在我們可以將這些函式放入名為 JSONDataStore 的類別,並在 Compose 應用程式中使用。

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }

// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
    Text("increment")
}

Proto DataStore

現在我們可以將這些函式放入名為 ProtoDataStore 的類別,並在 Compose 應用程式中使用。

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }

// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Counter $exampleCounter",
    fontSize = 25.sp
)

// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
    Text("increment")
}

在同步程式碼中使用 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 提供:

  • 讀取作業只會傳回已保存到磁碟的資料。
  • 寫入後的讀取作業一致性。
  • 寫入作業會序列化。
  • 讀取作業絕不會遭到寫入作業封鎖。

假設有一個範例應用程式,其中包含服務和活動,而服務是在個別程序中執行,並定期更新 DataStore。

本範例使用 JSON 資料儲存庫,但您也可以使用偏好設定或 Proto 資料儲存庫。

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

序列化器會通知 DataStore 如何讀取及寫入您的資料類型。請確認您尚未建立序列化器要使用的預設值,如果您還尚未建立任何檔案的話。以下是使用 kotlinx.serialization 的實作範例:

object TimeSerializer : Serializer<Time> {

    override val defaultValue: Time = Time(lastUpdateMillis = 0L)

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

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

如要在不同程序中使用 DataStore,您需要使用應用程式和服務程式碼的 MultiProcessDataStoreFactory 建構 DataStore 物件:

val dataStore = MultiProcessDataStoreFactory.create(
    serializer = TimeSerializer,
    produceFile = {
        File("${context.cacheDir.path}/time.pb")
    },
    corruptionHandler = null
)

請將以下內容新增到 AndroidManifiest.xml

<service
    android:name=".TimestampUpdateService"
    android:process=":my_process_id" />

服務會定期呼叫 updateLastUpdateTime(),後者會使用 updateData 寫入資料儲存庫。

suspend fun updateLastUpdateTime() {
    dataStore.updateData { time ->
        time.copy(lastUpdateMillis = System.currentTimeMillis())
    }
}

應用程式會使用資料流程讀取服務寫入的值:

fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
    time.lastUpdateMillis
}

現在,我們可以將所有這些函式放在名為 MultiProcessDataStore 的類別中,並在應用程式中使用。

服務代碼如下:

class TimestampUpdateService : Service() {
    val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
    val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }


    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        serviceScope.launch {
            while (true) {
                multiProcessDataStore.updateLastUpdateTime()
                delay(1000)
            }
        }
        return START_NOT_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}

以及應用程式程式碼:

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }

// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
    .collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
    text = "Last updated: $lastUpdateTime",
    fontSize = 25.sp
)

DisposableEffect(context) {
    val serviceIntent = Intent(context, TimestampUpdateService::class.java)
    context.startService(serviceIntent)
    onDispose {
        context.stopService(serviceIntent)
    }
}

您可以使用 Hilt 插入依附元件,確保每個程序都有專屬的 DataStore 執行個體:

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

處理檔案毀損問題

在極少數情況下,DataStore 的磁碟上永久檔案可能會毀損。根據預設,DataStore 不會自動從損毀狀態復原,嘗試從中讀取資料會導致系統擲回 CorruptionException

DataStore 提供損毀處理常式 API,可協助您在這種情況下順利復原,並避免擲回例外狀況。設定完成後,損毀處理常式會將損毀的檔案替換為包含預先定義預設值的新檔案。

如要設定這個處理常式,請在 by dataStore() 中建立 DataStore 例項時,或在 DataStoreFactory 工廠方法中提供 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,請參閱下列其他資源:

範例

網誌

程式碼研究室