SharedPreferences sudah lama menjadi pilihan default untuk menyimpan key-value sederhana di Android — flag feature, pengaturan pengguna, dan token autentikasi. Sayangnya, API SharedPreferences bersifat blocking (memuat seluruh file XML di memori saat getSharedPreferences dipanggil) dan rentan race condition saat ditulis dari beberapa thread. Jetpack DataStore hadir sebagai pengganti modern dengan dua varian: Preferences DataStore (key-value, mirip SharedPreferences) dan Proto DataStore (type-safe via Protocol Buffers). Bab ini menjelaskan kedua varian, API async berbasis Flow, dan migrasi dari SharedPreferences.
Preferences DataStore #
Preferences DataStore menyimpan key-value pair dengan key berupa Preferences.Key<T>. Key dibuat lewat booleanPreferencesKey(), stringPreferencesKey(), intPreferencesKey(), dst. Semua operasi baca-tulis berbasis Flow dan suspend, sehingga tidak ada blocking thread UI.
object Settings {
val THEME = stringPreferencesKey("theme")
val NOTIFIKASI = booleanPreferencesKey("notifikasi_enabled")
}
val Context.dataStore by preferencesDataStore("settings")
suspend fun setTheme(context: Context, theme: String) {
context.dataStore.edit { it[Settings.THEME] = theme }
}
fun observeTheme(context: Context): Flow<String> =
context.dataStore.data.map { it[Settings.THEME] ?: "system" }Method edit { } menerima lambda yang menerima MutablePreferences; perubahan disimpan secara atomic. Read lewat dataStore.data menghasilkan Flow<Preferences> yang mengeluarkan nilai terbaru setiap kali ada perubahan.
Proto DataStore #
Preferences DataStore tetap memakai string sebagai key dan tidak type-safe — bila kita salah kirim String ke key yang seharusnya Int, runtime akan crash dengan ClassCastException. Proto DataStore mengatasi ini dengan menyimpan objek Kotlin langsung (di-serialize ke protobuf). Kita mendefinisikan schema lewat .proto file, lalu DataStore generator menghasilkan class Kotlin.
data class PengaturanPengguna(
val theme: String = "system",
val notifikasiEnabled: Boolean = true,
val bahasa: String = "id-ID"
)
val Context.pengaturanDataStore: DataStore<PengaturanPengguna> by dataStore(
file = "pengaturan.pb",
serializer = PengaturanPenggunaSerializer
)
suspend fun setTheme(context: Context, theme: String) {
context.pengaturanDataStore.update { it.copy(theme = theme) }
}Keuntungan utama Proto DataStore: tidak ada cast manual, IDE autocomplete penuh, dan refactor aman. Kekurangannya: kita perlu mendefinisikan .proto dan men-generate serializer.
API Async Berbasis Flow #
Tabel berikut membandingkan API SharedPreferences, Preferences DataStore, dan Proto DataStore:
| Operasi | SharedPreferences | Preferences DataStore | Proto DataStore |
|---|---|---|---|
| Read satu key | getString(key, default) (blocking) | data.map { it[key] }.first() | data.map { it.field }.first() |
| Read observable | Tidak ada | data: Flow<Preferences> | data: Flow<T> |
| Write | edit().putString().apply() | edit { } (suspend) | update { } (suspend) |
| Thread-safe | Tidak atomic | Atomic (serialized writes) | Atomic (serialized writes) |
Simulasi DataStore #
Cuplikan berikut memodelkan Preferences DataStore dan Proto DataStore sebagai class dengan MutableStateFlow sebagai backend. Flow ter-update otomatis saat edit atau update dipanggil:
// data_store.kt
// Mensimulasikan DataStore<Preferences> (pengganti SharedPreferences) dengan
// API async, serta Proto DataStore type-safe. Tidak memerlukan Android SDK.
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
// Keys type-safe untuk Preferences DataStore
object PreferenceKeys {
val THEME = "theme"
val NOTIFIKASI = "notifikasi_enabled"
val BAHASA = "bahasa"
}
// Preferences DataStore: async, write serial
class PreferencesDataStore(private val storage: MutableMap<String, Any?> = mutableMapOf()) {
private val state = MutableStateFlow(storage.toMap())
val data: Flow<Map<String, Any?>> = state
fun edit(transform: (MutableMap<String, Any?>) -> Unit) {
val copy = storage.toMutableMap()
transform(copy)
storage.clear(); storage.putAll(copy)
state.value = storage.toMap()
}
fun <T> get(key: String, default: T): Flow<T> = data.map { (it[key] as? T) ?: default }
}
// Model type-safe untuk Proto DataStore
data class PengaturanPengguna(
val theme: String = "system",
val notifikasiEnabled: Boolean = true,
val bahasa: String = "id-ID"
)
// Proto DataStore: write langsung dengan tipe data lengkap
class ProtoDataStore(initial: PengaturanPengguna = PengaturanPengguna()) {
private val state = MutableStateFlow(initial)
val data: Flow<PengaturanPengguna> = state
fun update(transform: (PengaturanPengguna) -> PengaturanPengguna) {
state.value = transform(state.value)
}
}
fun main() = runBlocking {
println("=== Preferences DataStore ===")
val pref = PreferencesDataStore()
pref.edit { it[PreferenceKeys.THEME] = "dark"; it[PreferenceKeys.NOTIFIKASI] = true }
pref.edit { it[PreferenceKeys.BAHASA] = "id-ID" }
val theme: String = pref.get(PreferenceKeys.THEME, "system").first()
val bahasa: String = pref.get(PreferenceKeys.BAHASA, "en-US").first()
val notif: Boolean = pref.get(PreferenceKeys.NOTIFIKASI, false).first()
println("theme=$theme bahasa=$bahasa notifikasi=$notif")
println("snapshot data: ${pref.data.first()}")
println("\n=== Proto DataStore (type-safe) ===")
val proto = ProtoDataStore(PengaturanPengguna())
proto.update { it.copy(theme = "dark", notifikasiEnabled = false) }
val pengaturan = proto.data.first()
println("Pengaturan: $pengaturan")
println("\n=== SharedPreferences (perbandingan, blocking) ===")
val sharedPrefs = mutableMapOf<String, Any?>()
sharedPrefs["theme"] = "dark" // sync write, blocking
println("SharedPrefs: theme=${sharedPrefs["theme"]}")
}Output yang Dihasilkan:
=== Preferences DataStore ===
theme=dark bahasa=id-ID notifikasi=true
snapshot data: {theme=dark, notifikasi_enabled=true, bahasa=id-ID}
=== Proto DataStore (type-safe) ===
Pengaturan: PengaturanPengguna(theme=dark, notifikasiEnabled=false, bahasa=id-ID)
=== SharedPreferences (perbandingan, blocking) ===
SharedPrefs: theme=darkPerhatikan bahwa setelah edit memperbarui map, snapshot data.first() langsung merefleksikan perubahan. Demikian pula Proto DataStore yang memperbarui state.value langsung terlihat oleh pemanggil data.first().
Referensi Lanjutan #
- DataStore
— dokumentasi resmi pustaka DataStore Jetpack sebagai pengganti
SharedPreferencesberbasis coroutine
Selanjutnya #
DataStore cocok untuk preferensi. Untuk komunikasi dengan server REST, kita perlu Retrofit. Bab Networking dan Retrofit akan membahas setup Retrofit, anotasi @GET/@POST, dan integrasi OkHttp interceptor.