Android DataStore 數據存儲

在 Android 開發中,持久化簡單的輕量資料(如使用者設定、主題切換)過去大多使用 SharedPreferences。然而,SharedPreferences 存在許多架構上的缺陷,例如同步 API 可能導致主執行緒卡頓 (ANR)、缺乏錯誤處理機制以及不支援安全性型別。

Jetpack DataStore 是 Google 推薦的新一代資料儲存解決方案,它基於 Kotlin 協程 (Coroutines) 與 Flow 建構,完全是非同步、執行緒安全且具有交易原子性的。

SharedPreferences vs. DataStore

特性SharedPreferencesDataStore
執行緒模型同步 API (讀取可能導致 ANR)非同步 (基於協程 與 Flow)
錯誤處理無 (讀取失敗只會回傳預設值)有 (使用 Flow 的 catch 捕捉異常)
型別安全弱 (Key-Value)強 (Proto DataStore)
資料一致性弱 (無交易保證)強 (具原子性和交易保證)
遷移支援有 (內建 SharedPreferences 遷移)

依賴配置

首先,在 build.gradle 中加入 Preferences DataStore 依賴:

dependencies {
    implementation("androidx.datastore:datastore-preferences:1.1.1")
}

選擇 DataStore 類型

DataStore 提供兩種不同的實作:

  1. Preferences DataStore:與 SharedPreferences 類似,使用 Key 儲存資料,不需要事先定義結構 (Schema)。適合大多數簡單設定。
  2. Proto DataStore:儲存自定義的物件型態(使用 Protocol Buffers 定義),提供完整的型別安全。適合資料結構較複雜的場景。

本文重點介紹最常用的 Preferences DataStore

建立與定義 DataStore

宣告實例

通常在 Context 上透過擴充屬性 (Delegate) 來建立 DataStore 實例。

// settings_preferences.kt
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

// 定義 Key
object SettingsKeys {
    val THEME_MODE = booleanPreferencesKey("theme_mode")
    val USER_NAME = stringPreferencesKey("user_name")
}

讀取資料

DataStore 使用 Flow 來監聽資料變更。如果資料發生變化,Flow 會自動發射新的值。

val userThemeFlow: Flow<Boolean> = context.dataStore.data
    .catch { exception ->
        // 處理讀取時的 IO 異常
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }
    .map { preferences ->
        // 如果沒有值,則回傳預設值 (false = Light Mode)
        preferences[SettingsKeys.THEME_MODE] ?: false
    }

寫入資料

寫入操作必須在協程中執行。edit 函式確保了變更的原子性。

suspend fun toggleTheme(isDarkMode: Boolean) {
    context.dataStore.edit { preferences ->
        preferences[SettingsKeys.THEME_MODE] = isDarkMode
    }
}

從 SharedPreferences 遷移資料

如果你的舊 App 已經使用了 SharedPreferences,DataStore 提供了一個非常簡單的遷移路徑。

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        // 自動將舊的 SharedPreferences 資料搬移到 DataStore
        listOf(SharedPreferencesMigration(context, "old_prefs_name"))
    }
)

與 Hilt 及 ViewModel 整合

在現代架構中,我們會透過 Hilt 注入 DataStore,並在 ViewModel 中處理資料邏輯。

Hilt Module 提供 DataStore

@Module
@InstallIn(SingletonComponent::class)
object DataStoreModule {
    @Provides
    @Singleton
    fun provideDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
        return context.dataStore
    }
}

ViewModel 使用 Flow

@HiltViewModel
class SettingsViewModel @Inject constructor(
    private val dataStore: DataStore<Preferences>
) : ViewModel() {

    // 將 Flow 轉換為 StateFlow 供 UI 使用
    val themeState: StateFlow<Boolean> = dataStore.data
        .map { it[SettingsKeys.THEME_MODE] ?: false }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = false
        )

    fun updateTheme(isDarkMode: Boolean) {
        viewModelScope.launch {
            dataStore.edit { it[SettingsKeys.THEME_MODE] = isDarkMode }
        }
    }
}

透過切換到 DataStore,我們不僅解決了 SharedPreferences 引發的性能隱憂,更讓 App 的資料流符合響應式編程 (Reactive Programming) 的趨勢。