DataStore   Bagian dari Android Jetpack.

Coba dengan Multiplatform Kotlin
Multiplatform Kotlin memungkinkan berbagi lapisan data dengan platform lain. Pelajari cara menyiapkan dan menggunakan DataStore di KMP

Jetpack DataStore adalah solusi penyimpanan data yang memungkinkan Anda menyimpan key-value pair atau objek yang diketik dengan buffering protokol. DataStore menggunakan coroutine Kotlin dan Flow untuk menyimpan data secara asinkron, konsisten, dan transaksional.

Jika Anda menggunakan SharedPreferences untuk menyimpan data, sebaiknya bermigrasilah ke DataStore.

DataStore API

Antarmuka DataStore menyediakan API berikut:

  1. Alur yang dapat digunakan untuk membaca data dari DataStore

    val data: Flow<T>
    
  2. Fungsi untuk memperbarui data di DataStore

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

Konfigurasi DataStore

Jika Anda ingin menyimpan dan mengakses data menggunakan kunci, gunakan implementasi Preferences DataStore yang tidak memerlukan skema yang telah ditetapkan sebelumnya, dan tidak memberikan keamanan jenis. API ini memiliki API seperti SharedPreferences, tetapi tidak memiliki kelemahan yang terkait dengan preferensi bersama.

DataStore memungkinkan Anda mempertahankan class kustom. Untuk melakukannya, Anda harus menentukan skema untuk data dan menyediakan Serializer untuk mengonversinya ke format yang dapat dipertahankan. Anda dapat memilih untuk menggunakan Buffering Protokol, JSON, atau strategi serialisasi lainnya.

Penyiapan

Untuk menggunakan Jetpack DataStore di aplikasi, tambahkan berikut ini ke file Gradle Anda bergantung pada implementasi yang ingin digunakan:

Preferences DataStore

Tambahkan baris berikut ke bagian dependensi file gradle Anda:

Groovy

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

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

Kotlin

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

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

Untuk menambahkan dukungan RxJava opsional, tambahkan dependensi berikut:

Groovy

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

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

Kotlin

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

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

DataStore

Tambahkan baris berikut ke bagian dependensi file gradle Anda:

Groovy

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

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

Kotlin

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

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

Tambahkan dependensi opsional berikut untuk dukungan RxJava:

Groovy

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

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

Kotlin

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

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

Untuk melakukan serialisasi konten, tambahkan dependensi untuk serialisasi JSON atau Buffering Protokol.

Serialisasi JSON

Untuk menggunakan serialisasi JSON, tambahkan kode berikut ke file Gradle Anda:

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")
    }
    

Serialisasi Protobuf

Untuk menggunakan serialisasi Protobuf, tambahkan kode berikut ke file Gradle Anda:

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")
                }
            }
        }
    }
    

Menggunakan DataStore dengan benar

Agar dapat menggunakan DataStore dengan benar, selalu perhatikan aturan berikut:

  1. Jangan pernah membuat lebih dari satu instance DataStore untuk file tertentu dengan proses yang sama. Tindakan ini dapat merusak semua fungsi DataStore. Jika ada beberapa DataStore yang aktif untuk file tertentu dalam proses yang sama, DataStore akan menampilkan IllegalStateException saat membaca atau memperbarui data.

  2. Jenis DataStore<T> umum harus tidak dapat diubah. Mengubah jenis yang digunakan di DataStore akan membatalkan konsistensi yang diberikan DataStore dan juga membuat bug serius yang berpotensi sulit ditemukan. Sebaiknya gunakan buffering protokol, yang membantu memastikan immutabilitas, API yang jelas, dan serialisasi yang efisien.

  3. Jangan mencampur penggunaan SingleProcessDataStore dan MultiProcessDataStore untuk file yang sama. Jika Anda ingin mengakses DataStore dari beberapa proses, Anda harus menggunakan MultiProcessDataStore.

Definisi Data

Preferences DataStore

Tentukan kunci yang akan digunakan untuk mempertahankan data ke disk.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

Untuk JSON datastore, tambahkan anotasi @Serialization ke data yang ingin Anda pertahankan.

@Serializable
data class Settings(
    val exampleCounter: Int
)

Tentukan class yang mengimplementasikan Serializer<T>, dengan T adalah jenis class yang Anda tambahkan anotasi sebelumnya. Pastikan Anda menyertakan nilai default untuk serialisasi yang akan digunakan jika belum ada file yang dibuat.

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

Implementasi Proto DataStore menggunakan DataStore dan buffering protokol untuk mempertahankan objek yang diketik ke disk.

Proto DataStore memerlukan skema yang telah ditetapkan sebelumnya dalam file proto di direktori app/src/main/proto/. Skema ini menetapkan jenis objek yang Anda pertahankan di Proto DataStore. Untuk mempelajari lebih lanjut penetapan skema proto, baca panduan bahasa protobuf.

Tambahkan file bernama settings.proto di dalam folder src/main/proto:

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Tentukan class yang mengimplementasikan Serializer<T>, dengan T adalah jenis yang ditetapkan dalam file proto. Class penserialisasi ini menentukan cara DataStore membaca dan menulis jenis data Anda. Pastikan Anda menyertakan nilai default untuk serialisasi yang akan digunakan jika belum ada file yang dibuat.

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)
    }
}

Membuat DataStore

Anda perlu menentukan nama untuk file yang digunakan untuk mempertahankan data.

Preferences DataStore

Implementasi Preferences DataStore menggunakan class DataStore dan Preferences untuk mempertahankan key-value pair ke disk. Gunakan delegasi properti yang dibuat oleh preferencesDataStore untuk membuat instance DataStore<Preferences>. Panggil sekali di tingkat teratas file Kotlin Anda. Akses DataStore melalui properti ini di seluruh aplikasi Anda. Hal ini memudahkan Anda mempertahankan DataStore sebagai singleton. Parameter name wajib adalah nama Preferences DataStore.

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

JSON DataStore

Gunakan delegasi properti yang dibuat oleh dataStore untuk membuat instance DataStore<T>, dengan T adalah class data yang dapat diserialisasi. Panggil sekali di tingkat teratas file Kotlin Anda dan akses melalui delegasi properti ini di seluruh aplikasi Anda. Parameter fileName memberi tahu DataStore file mana yang akan digunakan untuk menyimpan data, dan parameter serializer memberi tahu DataStore nama class serialisasi yang ditentukan sebelumnya.

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

Proto DataStore

Gunakan delegasi properti yang dibuat oleh dataStore untuk membuat instance DataStore<T>, dengan T adalah jenis yang ditetapkan dalam file proto. Panggil sekali di tingkat teratas file Kotlin Anda dan akses melalui delegasi properti ini di seluruh aplikasi Anda. Parameter fileName memberi tahu DataStore file mana yang akan digunakan untuk menyimpan data, dan parameter serializer memberi tahu DataStore nama class serialisasi yang ditentukan sebelumnya.

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

Membaca dari DataStore

Anda perlu menentukan nama untuk file yang digunakan untuk mempertahankan data.

Preferences DataStore

Karena Preferences DataStore tidak menggunakan skema yang telah ditetapkan sebelumnya, Anda harus menggunakan fungsi jenis kunci yang sesuai untuk menentukan kunci untuk setiap nilai yang Anda perlu disimpan dalam instance DataStore<Preferences>. Misalnya, untuk menentukan kunci untuk nilai int, gunakan intPreferencesKey. Selanjutnya, gunakan properti DataStore.data untuk mengekspos nilai tersimpan yang sesuai menggunakan Flow.

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

JSON DataStore

Gunakan DataStore.data untuk mengekspos Flow properti yang sesuai dari objek yang disimpan.

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

Proto DataStore

Gunakan DataStore.data untuk mengekspos Flow properti yang sesuai dari objek yang disimpan.

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

Gunakan collectAsStateWithLifecycle untuk menggunakan Flow yang dihasilkan oleh ViewModel dalam composable. Tindakan ini akan mengonversi DataStore Flow dengan aman menjadi Compose State yang memicu rekomposisi.

@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
  val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
  Text(text = "Example counter: ${counter}")
}

Untuk mengetahui informasi selengkapnya tentang collectAsStateWithLifecycle, lihat Status dan Jetpack Compose.

Menulis ke DataStore

DataStore menyediakan fungsi updateData yang mengupdate objek yang disimpan secara transaksional. updateData memberi Anda status data saat ini sebagai instance jenis data dan mengupdate data secara transaksional dalam operasi baca-tulis-modifikasi atomik. Semua kode dalam blok updateData diperlakukan sebagai transaksi tunggal.

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 }
    }
}

Menggunakan DataStore di aplikasi Compose

Untuk menggunakan DataStore di aplikasi Compose, ikuti panduan arsitektur aplikasi Android dengan menyimpan operasi DataStore di lapisan data Anda (seperti repositori) dan mengekspos data ke UI Anda melalui ViewModel.

Hindari membaca dari atau menulis ke DataStore secara langsung dalam fungsi composable Anda.

  1. Ekspos DataStore melalui ViewModel. Teruskan repositori Anda (yang menggabungkan DataStore) ke ViewModel dan konversi Flow ke StateFlow sehingga UI dapat mengamatinya dengan mudah, seperti yang ditunjukkan dalam cuplikan berikut:

    class SettingsViewModel(
        private val userPreferencesRepository: UserPreferencesRepository
    ) : ViewModel() {
    
        // Expose the DataStore flow as a StateFlow for Compose
        val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = UserSettings.getDefaultInstance()
            )
    
        fun updateCounter(newValue: Int) {
            viewModelScope.launch {
                userPreferencesRepository.updateCounter(newValue)
            }
        }
    }
    
  2. Amati dan tulis dari composable Anda. Gunakan collectAsStateWithLifecycle untuk mengamati StateFlow dengan aman di UI Anda, dan panggil fungsi ViewModel untuk menangani penulisan, seperti yang ditunjukkan dalam cuplikan berikut:

    @Composable
    fun SettingsScreen(
        viewModel: SettingsViewModel = viewModel()
    ) {
        // Safely collect the state
        val settings by viewModel.userSettings.collectAsStateWithLifecycle()
    
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Current counter: ${settings.counter}")
    
            Spacer(modifier = Modifier.height(8.dp))
    
            Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) {
                Text("Increment Counter")
            }
        }
    }
    

Menggunakan DataStore dalam kode multiproses

Anda dapat mengonfigurasi DataStore untuk mengakses data yang sama di berbagai proses, dan properti konsistensi data terjamin tetap sama seperti berasal dari dalam satu proses. Secara khusus, DataStore menyediakan properti berikut:

  • Operasi baca hanya menampilkan data yang telah disimpan ke disk.
  • Konsistensi operasi baca setelah tulis.
  • Penulisan diserialisasi.
  • Operasi baca tidak pernah diblokir oleh operasi tulis.

Sebaiknya satu aplikasi contoh berisi satu layanan dan satu aktivitas, dengan layanan berjalan dalam proses terpisah dan secara berkala memperbarui DataStore.

Contoh ini menggunakan JSON datastore, tetapi Anda juga dapat menggunakan Preferences atau Proto DataStore.

@Serializable
data class Time(
    val lastUpdateMillis: Long
)

Serializer memberi tahu DataStore cara membaca dan menulis jenis data Anda. Pastikan Anda menyertakan nilai default untuk serialisasi yang akan digunakan jika belum ada file yang dibuat. Berikut adalah contoh implementasi yang menggunakan 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()
        )
    }
}

Agar dapat menggunakan DataStore di berbagai proses, Anda harus membuat objek DataStore menggunakan MultiProcessDataStoreFactory untuk kode aplikasi dan layanan:

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

Tambahkan kode berikut ke AndroidManifiest.xml Anda:

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

Layanan secara berkala memanggil updateLastUpdateTime, yang menulis ke datastore menggunakan updateData.

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

Aplikasi membaca nilai yang ditulis oleh layanan menggunakan aliran data:

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

Sekarang, kita dapat menggabungkan semua fungsi ini dalam class bernama MultiProcessDataStore dan menggunakannya di Aplikasi.

Berikut kode layanan:

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()
    }
}

Dan kode aplikasi:

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)
    }
}

Anda dapat menggunakan Hilt injeksi dependensi sehingga instance DataStore Anda bersifat unik untuk setiap proses:

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

Menangani kerusakan file

Terkadang, file persisten DataStore di disk dapat rusak. Secara default, DataStore tidak otomatis pulih dari kerusakan, dan upaya untuk membacanya akan menyebabkan sistem menampilkan CorruptionException.

DataStore menawarkan corruption handler API yang dapat membantu Anda pulih dengan lancar dalam skenario seperti itu, dan menghindari menampilkan pengecualian. Jika dikonfigurasi, corruption handler akan mengganti file yang rusak dengan file baru yang berisi nilai default yang telah ditetapkan sebelumnya.

Untuk menyiapkan handler ini, berikan corruptionHandler saat membuat instance DataStore di by dataStore atau di metode factory DataStoreFactory:

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.filesDir.path}/myapp.preferences_pb")
   },
   corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Berikan masukan

Sampaikan masukan dan ide Anda kepada kami melalui resource berikut:

Pelacak masalah:
Laporkan masalah agar kami dapat memperbaiki bug.

Referensi lainnya

Untuk mempelajari Jetpack DataStore lebih lanjut, lihat referensi tambahan berikut:

Contoh

Blog

Codelab