Arsitektur Aplikasi: Lapisan Data - DataStore - Android Developers

Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: Jelajahi panduan arsitektur aplikasi ini tentang library lapisan data untuk mempelajari Preferensi DataStore dan Proto DataStore, Penyiapan, dan lainnya. hide_page_heading: true

DataStore   Bagian dari Android Jetpack.

Coba dengan Kotlin Multiplatform
Kotlin Multiplatform 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. Memiliki API seperti SharedPreferences, tetapi tidak memiliki kekurangan 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 Protocol Buffers, 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.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")
    }
    

Untuk menambahkan dukungan RxJava opsional, tambahkan dependensi berikut:

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

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

Tambahkan dependensi opsional berikut untuk dukungan 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")
    }
    

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 generik DataStore<T> harus tidak dapat diubah. Mengubah jenis yang digunakan di DataStore akan membatalkan konsistensi yang diberikan DataStore dan membuat bug serius yang berpotensi sulit ditemukan. Sebaiknya gunakan buffering protokol, yang membantu memastikan tidak dapat diubah, API yang jelas, dan serialisasi yang efisien.

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

Definisi Data

Preferences DataStore

Tentukan kunci yang akan digunakan untuk menyimpan data ke disk.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")

JSON DataStore

Untuk penyimpanan data JSON, 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, lihat 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 serializer 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 harus 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. Atau, gunakan RxPreferenceDataStoreBuilder jika Anda menggunakan RxJava. 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 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 di langkah 1.

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 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 penserialisasi yang ditentukan di langkah 1.

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

Membaca dari DataStore

Anda harus 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 bagi setiap nilai yang perlu disimpan dalam instance DataStore<Preferences>. Misalnya, untuk menentukan kunci nilai int, gunakan intPreferencesKey(). Kemudian, 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
}

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 satu transaksi.

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

Contoh Compose

Anda dapat menggabungkan fungsi-fungsi ini dalam class dan menggunakannya di aplikasi Compose.

Preferences DataStore

Sekarang kita dapat memasukkan fungsi ini ke dalam class bernama PreferencesDataStore dan menggunakannya di Aplikasi 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

Sekarang kita dapat memasukkan fungsi ini ke dalam class bernama JSONDataStore dan menggunakannya di Aplikasi 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

Sekarang kita dapat memasukkan fungsi ini ke dalam class bernama ProtoDataStore dan menggunakannya di Aplikasi 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")
}

Menggunakan DataStore dalam kode sinkron

Salah satu manfaat utama DataStore adalah API asinkron, tetapi tidak selalu memungkinkan untuk mengubah kode di sekitarnya menjadi asinkron. Hal ini mungkin terjadi jika Anda menangani codebase yang ada yang menggunakan I/O disk sinkron atau jika Anda memiliki dependensi yang tidak menyediakan API asinkron.

Coroutine Kotlin menyediakan builder coroutine runBlocking() untuk membantu menjembatani jarak antara kode sinkron dan asinkron. Anda dapat menggunakan runBlocking() untuk membaca data dari DataStore secara sinkron. RxJava menawarkan metode pemblokiran di Flowable. Kode berikut memblokir thread panggilan hingga DataStore menampilkan data:

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

Menjalankan operasi I/O sinkron pada UI thread dapat menyebabkan ANR atau UI tidak responsif. Anda dapat mencegah masalah ini dengan melakukan pramuat data secara asinkron dari DataStore:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

Java

dataStore.data().first().subscribe();

Dengan demikian, DataStore secara asinkron membaca data dan meng-cache-nya dalam memori. Selanjutnya, pembacaan sinkron menggunakan runBlocking() dapat lebih cepat atau menghindari operasi I/O disk sepenuhnya jika pembacaan awal telah selesai.

Menggunakan DataStore dalam kode multiproses

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

  • 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.

Pertimbangkan aplikasi contoh dengan layanan dan aktivitas tempat layanan berjalan dalam proses terpisah dan secara berkala memperbarui DataStore.

Contoh ini menggunakan datastore JSON, tetapi Anda juga dapat menggunakan datastore preferensi atau proto.

@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 penerapan 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 aplikasi dan kode layanan:

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

Tambahkan kode berikut ke AndroidManifiest.xml Anda:

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

Layanan ini 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 alur 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 layanannya:

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 injeksi dependensi Hilt agar 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 di disk DataStore dapat rusak. Secara default, DataStore tidak otomatis pulih dari kerusakan, dan upaya untuk membaca darinya akan menyebabkan sistem memunculkan CorruptionException.

DataStore menawarkan API handler kerusakan yang dapat membantu Anda memulihkan dengan baik dalam skenario tersebut, dan menghindari pengecualian. Jika dikonfigurasi, penangan kerusakan akan mengganti file yang rusak dengan file baru yang berisi nilai default yang telah ditentukan sebelumnya.

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

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

Berikan masukan

Sampaikan masukan dan ide Anda kepada kami melalui resource berikut:

Issue tracker:
Laporkan masalah agar kami dapat memperbaiki bug.

Referensi lainnya

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

Contoh

Blog

Codelab