16.1 Preferences DataStore

DataStore slúži na perzistovanie malých dát. Perzistovanie znamená, že dáta, ktoré si do DataStore uložím, zostanú k dispozícii aj po aplikácii sa ukončí/reštartuje. Ukladáme tam veci ako:

  1. nastavenia aplikácie
  2. autentikačné tokeny (dáta prihláseného používateľa)
  3. informáciu o absolvovaní onboardingu a pod.

Dáta sa ukladajú štýlom kľúč hodnota. Teda napríklad pod kľúčom "DARK_MODE bude uložená hodnota true alebo false. Vždy keď si vyžiadam hodnotu pod kľúčom "DARK_MODE", DataStore vráti hodnotu, ktorá tam bola uložená naposledy. Dáta z DataStore Preferences sa za normálnych okolností neprenášajú medzi inštaláciami aplikácie.

Preferences DataStore Setup

Pridáme závislosť do súboru app/build.gradle.kts:

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

Vytvorenie kľúčov

Kľúče definujeme tak, že priamo podľa kľúča vieme zistiť, aký typ dát sa nachádza v hodnote. Tieto kľúče ako aj zvyšok konfigurácie odporúčam držať v samostatnom súbore. Napr. preferences.kt. Zopár praktických príkladov:

import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val EXAMPLE_TOKEN = stringPreferencesKey("example_token")
val EXAMPLE_ONBOARDING_PASSED = booleanPreferencesKey("example_onboarding_passed")
val EXAMPLE_LAST_UPDATE_MILLIS = longPreferencesKey("example_last_update_millis")

Získanie Preferences DataStore

DataStore preferences sú na pozadí uložené v nejakom súbore. Keď chceme získať DataStore pre prácu s preferences, použijeme preferencesDataStore s názvom tohto súboru (názov je na nás):

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

Ak chceme získať Context v Composable funkcii, voláme LocalContext.current. Napríklad nasledovne:

@Composable
fun DataStoreSample() {
    val context = LocalContext.current
    val dataStore = context.dataStore
}

Práca s Preferences DataStore

Pre prácu s Preferences DataStore si rád definujem pomocné funkcie, lebo práca s ním môže byť nepríjemná:

fun <T> DataStore<Preferences>.observe(key: Preferences.Key<T>): Flow<T?> = data.map { prefs -> prefs[key] }

suspend fun <T> DataStore<Preferences>.set(key: Preferences.Key<T>, value: T) = updateData { prefs ->
    prefs.toMutablePreferences().apply { set(key, value) }
}

suspend fun <T> DataStore<Preferences>.clear(key: Preferences.Key<T>) = updateData { prefs ->
    prefs.toMutablePreferences().apply { remove(key) }
}

Všimnime si, že funkcia observe vracia Flow a nie priamo hodnotu. To znamená jednoducho to, že akonáhle sa zmení hodnota v DataStore, funkcia observe okamžite vráti novú hodnotu. Môžeme tak okamžite aktualizovať UI pri zmene hodnoty v DataStore.

Ukážka na počúvanie zmien v DatasStore:

@Composable
fun DataStoreSample() {
    val context = LocalContext.current
    val dataStore = context.dataStore

    val counter by dataStore.observe(EXAMPLE_COUNTER).collectAsState(initial = 0)
    val token by dataStore.observe(EXAMPLE_TOKEN).collectAsState(initial = "")
    val onboardingPassed by dataStore.observe(EXAMPLE_ONBOARDING_PASSED).collectAsState(initial = false)
    val lastUpdateMillis by dataStore.observe(EXAMPLE_LAST_UPDATE_MILLIS).collectAsState(initial = 0L)

    // Tento Text sa aktualizuje pri zmene ktorejkolvek zo 4 hodnot v riadkoch vyssie
    Text(
        "Counter: $counter, \n" +
                "Token: $token, \n" +
                "Onboarding Passed: $onboardingPassed, \n" +
                "Last Update: $lastUpdateMillis\n"
    )
}

Ukážka na zapisovanie hodnot do Preferences DataStore - pre zapisovanie potrebujeme volať suspend funkciu, preto musíme použiť rememberCoroutineScope (alebo s nimi pracovať vo ViewModel):

@Composable
fun DataStoreSample() {
    val context = LocalContext.current
    val dataStore = context.dataStore
    val coroutineScope = rememberCoroutineScope()

    Button(onClick = {
        coroutineScope.launch {
            dataStore.set(EXAMPLE_COUNTER, 0)
        }
    }) {
        Text("Clear Counter")
    }
}

Akonáhle by sme chceli riešiť napríklad inkrementáciu hodnoty, mnou definované pomocné funkcie by nestačili. Tie totiž nepoznajú predchádzajúcu hodnotu. Buď si nechaj riešenie vygenerovať, alebo sa mi ozvi :)