Menangani Preferences DataStore

Apa itu DataStore?

DataStore adalah solusi penyimpanan data yang baru dan lebih baik yang ditujukan untuk menggantikan SharedPreferences. Dibuat di coroutine Kotlin dan Flow, DataStore menyediakan dua implementasi berbeda: Proto DataStore yang menyimpan objek berjenis (didukung oleh buffering protokol) dan Preferences DataStore yang menyimpan key-value pair. Data disimpan secara asinkron, konsisten, dan transaksional, yang mengatasi beberapa kelemahan SharedPreferences.

Yang akan Anda pelajari

  • Pengertian DataStore dan alasan harus menggunakannya.
  • Cara menambahkan DataStore ke project Anda.
  • Perbedaan antara Preferences DataStore dan Proto DataStore, serta keunggulan keduanya.
  • Cara menggunakan Preferences DataStore.
  • Cara bermigrasi dari SharedPreferences ke Preferences DataStore.

Yang akan Anda buat

Dalam codelab ini, Anda akan memulai dengan aplikasi contoh yang menampilkan daftar tugas yang dapat difilter menurut status selesainya, serta dapat diurutkan menurut prioritas dan batas waktu.

fcb2ffa4e6b77f33.gif

Flag boolean untuk filter Tampilkan tugas yang telah selesai disimpan di memori. Tata urutan dipertahankan ke disk menggunakan objek SharedPreferences.

Dalam codelab ini, Anda akan mempelajari cara menggunakan Preferences DataStore dengan menyelesaikan tugas berikut:

  • Mempertahankan filter status selesai di DataStore.
  • Memigrasikan tata urutan dari SharedPreferences ke DataStore.

Sebaiknya gunakan juga codelab Proto DataStore agar Anda dapat lebih memahami perbedaan antara keduanya.

Yang Anda butuhkan

Untuk pengantar Komponen Arsitektur, lihat Room dengan codelab View. Untuk pengantar Flow, lihat Coroutine Lanjutan dengan Flow Kotlin dan codelab LiveData.

Dalam langkah ini, Anda akan mendownload kode untuk seluruh codelab, lalu menjalankan aplikasi contoh sederhana.

Agar dapat segera dimulai, kami telah menyiapkan proyek pemula untuk Anda kembangkan.

Jika sudah menginstal git, Anda cukup menjalankan perintah di bawah. Untuk memeriksa apakah git sudah diinstal, ketik git --version di terminal atau command line dan pastikan git dijalankan dengan benar.

 git clone https://github.com/googlecodelabs/android-datastore

Status awal berada di cabang master. Kode solusi terletak di cabang preferences_datastore.

Jika tidak memiliki git, Anda dapat mengklik tombol berikut untuk mendownload semua kode untuk codelab ini:

Download kode sumber

  1. Buka zip kode, lalu buka project di Android Studio versi 3.6 atau yang lebih baru.
  2. Jalankan aplikasi yang menjalankan konfigurasi di perangkat atau emulator.

b3c0dfdb92dfed77.png

Aplikasi mulai berjalan dan menampilkan daftar tugas:

16eb4ceb800bf131.png

Aplikasi akan memungkinkan Anda melihat daftar tugas. Setiap tugas memiliki properti berikut: nama, status selesai, prioritas, dan batas waktu.

Untuk menyederhanakan kode yang perlu ditangani, aplikasi hanya mengizinkan Anda melakukan dua tindakan:

  • Menampilkan atau menyembunyikan visibilitas tugas yang telah selesai - tugas disembunyikan secara default
  • Mengurutkan tugas menurut prioritas, batas waktu, atau batas waktu dan prioritas

Aplikasi mengikuti arsitektur yang direkomendasikan di "Panduan arsitektur aplikasi". Anda akan menemukan item berikut di setiap paket:

data

  • Class model Task.
  • Class TasksRepository - bertanggung jawab untuk menyediakan tugas. Agar tidak rumit, metode ini akan menampilkan data yang di-hardcode dan mengeksposnya melalui Flow untuk menampilkan skenario yang lebih realistis.
  • Class UserPreferencesRepository - menampung SortOrder yang didefinisikan sebagai enum. Tata urutan saat ini disimpan di SharedPreferences sebagai String, berdasarkan nama nilai enum. Ini akan menampilkan metode sinkron untuk menyimpan dan mendapatkan tata urutan.

ui

  • Class yang terkait menampilkan Activity dengan RecyclerView.
  • Class TasksViewModel bertanggung jawab atas logika UI.

TasksViewModel - menampung semua elemen yang dibutuhkan untuk mem-build data yang perlu ditampilkan di UI: daftar tugas, flag tampilkan yang telah selesai dan tata urutan, yang digabungkan dalam objek TasksUiModel. Setiap kali salah satu nilai ini berubah, TasksUiModel yang baru harus direkonstruksi. Untuk dapat melakukannya, 3 elemen akan digabungkan:

  • Flow<List<Task>> yang diambil dari TasksRepository.
  • MutableStateFlow<Boolean> yang memiliki flag tampilkan yang telah selesai terakhir yang hanya disimpan dalam memori.
  • MutableStateFlow<SortOrder> yang memiliki nilai SortOrder terakhir.

Untuk memastikan bahwa UI telah diperbarui dengan benar, LiveData<TasksUiModel> akan diekspos hanya saat Aktivitas dimulai.

Kita memiliki beberapa masalah dengan kode:

  • Kita memblokir UI thread pada I/O disk saat menginisialisasi UserPreferencesRepository.sortOrder. Hal ini dapat mengakibatkan jank pada UI.
  • Flag tampilkan yang selesai hanya disimpan dalam memori, sehingga reset akan terjadi setiap kali pengguna membuka aplikasi. Seperti SortOrder, flag ini harus disimpan agar tetap ada saat aplikasi ditutup.
  • Saat ini kita menggunakan SharedPreferences untuk mempertahankan data, tetapi menyimpan MutableStateFlow dalam memori yang telah dimodifikasi secara manual agar dapat menerima notifikasi perubahan. Cara ini cenderung mudah mengalami gangguan jika nilai diubah di tempat lain di aplikasi.
  • Di UserPreferencesRepository, kita mengekspos dua metode untuk memperbarui tata urutan: enableSortByDeadline() dan enableSortByPriority(). Kedua metode tersebut bergantung pada nilai tata urutan saat ini. Namun, jika salah satu metode dipanggil sebelum metode lainnya selesai, nilai akhir akan salah. Bahkan, metode ini dapat mengakibatkan jank pada UI dan pelanggaran Mode Ketat saat metode dipanggil di UI thread.

Meskipun flag tampilkan yang telah selesai dan tata urutan adalah preferensi pengguna, saat ini keduanya ditampilkan sebagai dua objek yang berbeda. Oleh karena itu, salah satu tujuan kita adalah menyatukan dua flag ini dalam satu class UserPreferences.

Mari cari tahu cara menggunakan DataStore untuk membantu menangani masalah ini.

Sering kali Anda mungkin perlu menyimpan set data kecil atau sederhana. Sebelumnya, Anda mungkin telah menggunakan SharedPreferences, tetapi API ini juga memiliki serangkaian kelemahan. Library Jetpack DataStore bertujuan mengatasi masalah tersebut dan membuat API yang sederhana, aman, dan asinkron untuk menyimpan data. Library tersebut menyediakan 2 implementasi yang berbeda:

  • Preferences DataStore
  • Proto DataStore

Fitur

SharedPreferences

PreferencesDataStore

ProtoDataStore

API asinkron

✅ (hanya untuk membaca nilai yang diubah, melalui pemroses)

✅ (melalui Flow)

✅ (melalui Flow)

API sinkron

✅ (tetapi tidak aman untuk dipanggil di UI thread)

Aman untuk dipanggil di UI thread

❌*

✅ (tugas dipindahkan ke Dispatchers.IO di balik layar)

✅ (tugas dipindahkan ke Dispatchers.IO di balik layar)

Dapat memperingatkan adanya error

Aman dari pengecualian runtime

❌**

Memiliki API transaksional dengan jaminan konsistensi kuat

Menangani migrasi data

✅ (dari SharedPreferences)

✅ (dari SharedPreferences)

Keamanan jenis

✅ dengan Buffering Protokol

  • SharedPreferences memiliki API sinkron yang dapat terlihat aman untuk dipanggil di UI thread, tetapi sebenarnya melakukan operasi I/O disk. Selanjutnya, apply() akan memblokir UI thread di fsync(). Panggilan fsync() yang tertunda dipicu setiap kali layanan dimulai atau berhenti, dan setiap kali aktivitas dimulai atau berhenti di mana pun di aplikasi. UI thread diblokir saat panggilan fsync() tertunda yang dijadwalkan oleh apply(), dan sering menjadi sumber ANR.

** SharedPreferences menampilkan error penguraian sebagai pengecualian runtime.

Preference DataStore vs Proto DataStore

Meskipun Preference DataStore dan Proto DataStore mengizinkan penyimpanan data, keduanya dilakukan dengan cara yang berbeda:

  • Preference DataStore, seperti SharedPreferences, mengakses data berdasarkan kunci, tanpa menentukan skema awal.
  • Proto DataStore menentukan skema menggunakan Buffering Protokol. Menggunakan Protobuf memungkinkan penyimpanan data dengan jenis yang dikenali. Protobuf ini lebih cepat, lebih kecil, lebih sederhana, dan tidak terlalu ambigu dibandingkan dengan XML dan format data serupa lainnya. Meskipun Proto DataStore mengharuskan Anda mempelajari mekanisme serialisasi baru, kami meyakini bahwa keunggulan jenis yang dikenali yang diberikan oleh Proto DataStore tersebut bermanfaat.

Room vs DataStore

Jika memerlukan update sebagian, integritas referensial, atau set data yang besar/kompleks, sebaiknya gunakan Room, bukan DataStore. DataStore cocok untuk set data kecil atau sederhana dan tidak mendukung update sebagian atau integritas referensial.

Preferences DataStore API mirip dengan SharedPreferences, tetapi memiliki beberapa perbedaan penting:

  • Menangani pembaruan data secara transaksional
  • Mengekspos Flow yang menampilkan status data saat ini
  • Tidak memiliki metode persisten data (apply(), commit())
  • Tidak menampilkan referensi yang dapat diubah ke status internalnya
  • Mengekspos API yang mirip dengan Map dan MutableMap dengan kunci yang diketik

Mari kita lihat cara menambahkannya ke project dan memigrasikan SharedPreferences ke DataStore.

Menambahkan dependensi

Update file build.gradle untuk menambahkan dependensi Preference DataStore berikut:

implementation "androidx.datastore:datastore-preferences:1.0.0-alpha06"

Meskipun flag tampilkan yang telah selesai dan tata urutan adalah preferensi pengguna, saat ini keduanya ditampilkan sebagai dua objek yang berbeda. Jadi, salah satu tujuan kita adalah menyatukan dua flag ini di class UserPreferences dan menyimpannya di UserPreferencesRepository menggunakan DataStore. Saat ini, flag tampilkan yang telah selesai disimpan di memori, di TasksViewModel.

Mari mulai dengan membuat class data UserPreferences di UserPreferencesRepository. Seharusnya saat ini hanya ada satu kolom: showCompleted. Kita akan menambahkan tata urutannya nanti.

data class UserPreferences(val showCompleted: Boolean)

Membuat DataStore

Mari buat kolom pribadi DataStore<Preferences> di UserPreferencesRepository, menggunakan metode context.createDataStoreFactory(). Parameter wajib adalah nama Preferences DataStore.

private val dataStore: DataStore<Preferences> =
        context.createDataStore(name = "user")

Membaca data dari Preferences DataStore

Preferences DataStore mengekspos data yang disimpan di Flow<Preferences> yang akan ditampilkan setiap kali preferensi berubah. Kita tidak ingin mengekspos seluruh objek Preferences, melainkan objek UserPreferences. Untuk melakukannya, kita harus memetakan Flow<Preferences>, mendapatkan nilai Boolean yang diinginkan, berdasarkan kunci dan membuat objek UserPreferences.

Jadi, hal pertama yang perlu kita lakukan adalah menentukan kunci show completed - ini adalah nilai booleanPreferencesKey yang dapat kita deklarasikan sebagai anggota dalam objek PreferencesKeys pribadi.

private object PreferencesKeys {
  val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
}

Mari mengekspos userPreferencesFlow: Flow<UserPreferences>, yang dikonstruksikan berdasarkan dataStore.data: Flow<Preferences>, yang kemudian dipetakan, untuk mengambil preferensi yang tepat:

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .map { preferences ->
        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
        UserPreferences(showCompleted)
    }

Menangani pengecualian saat membaca data

Saat DataStore membaca data dari file, IOExceptions akan muncul saat terjadi error selama pembacaan data. Kita dapat menanganinya dengan menggunakan operator Flow catch() sebelum map() dan memunculkan emptyPreferences() jika pengecualian yang ditampilkan adalah IOException. Jika jenis pengecualian lain ditampilkan, pilih tampilkan kembali.

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: false
        UserPreferences(showCompleted)
    }

Menulis data ke Preferences DataStore

Untuk menulis data, DataStore menawarkan fungsi DataStore.edit(transform: suspend (MutablePreferences) -> Unit) penangguhan, yang menerima blok transform yang memungkinkan kita memperbarui status di DataStore secara transaksional.

MutablePreferences yang diteruskan ke blok transformasi akan diperbarui dengan semua edit yang telah dijalankan sebelumnya. Semua perubahan pada MutablePreferences dalam blok transform akan diterapkan ke disk setelah transform selesai dan sebelum edit selesai. Menyetel satu nilai di MutablePreferences tidak akan mengubah semua preferensi lainnya.

Catatan: jangan mencoba mengubah MutablePreferences di luar blok transformasi.

Mari membuat fungsi penangguhan yang memungkinkan kita memperbarui properti showCompleted dari UserPreferences, disebut sebagai updateShowCompleted(), yang memanggil dataStore.edit() dan menyetel nilai baru:

suspend fun updateShowCompleted(showCompleted: Boolean) {
    dataStore.edit { preferences ->
        preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted
    }
}

edit() dapat menampilkan IOException jika terjadi error saat membaca atau menulis ke disk. Jika terjadi error lain dalam blok transformasi, error tersebut akan ditampilkan oleh edit().

Pada tahap ini, aplikasi harus dikompilasi, tetapi fungsi yang baru saja dibuat di UserPreferencesRepository tidak akan digunakan.

Tata urutan disimpan di SharedPreferences. Mari kita pindahkan ke DataStore. Untuk melakukannya, mulai dengan mengupdate UserPreferences untuk menyimpan tata urutan:

data class UserPreferences(
    val showCompleted: Boolean,
    val sortOrder: SortOrder
)

Bermigrasi dari SharedPreferences

Untuk memigrasikannya ke DataStore, kita perlu mengupdate builder dataStore agar dapat meneruskan SharedPreferencesMigration ke daftar migrasi. DataStore akan otomatis bermigrasi dari SharedPreferences ke DataStore. Migrasi akan dijalankan sebelum akses data apa pun dapat terjadi di DataStore. Artinya, migrasi Anda harus berhasil sebelum DataStore.data menampilkan nilai apa pun dan sebelum DataStore.edit() dapat memperbarui data.

Catatan: kunci hanya dimigrasikan dari SharedPreferences satu kali, jadi Anda harus berhenti menggunakan SharedPreferences lama setelah kode dimigrasikan ke DataStore.

private val dataStore: DataStore<Preferences> =
    context.createDataStore(
        name = USER_PREFERENCES_NAME,
        migrations = listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    )

private object PreferencesKeys {
    ...
    // Note: this has the the same name that we used with SharedPreferences.
    val SORT_ORDER = stringPreferencesKey("sort_order")
}

Semua kunci akan dimigrasikan ke DataStore kami dan dihapus dari SharedPreferences preferensi pengguna. Dari Preferences, sekarang kita dapat memperoleh dan mengupdate SortOrder berdasarkan kunci SORT_ORDER.

Membaca tata urutan dari DataStore

Mari memperbarui userPreferencesFlow untuk mengambil tata urutan dalam transformasi map():

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        // Get the sort order from preferences and convert it to a [SortOrder] object
        val sortOrder =
            SortOrder.valueOf(
                preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name)

        // Get our show completed value, defaulting to false if not set:
        val showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED] ?: false
        UserPreferences(showCompleted, sortOrder)
    }

Menyimpan tata urutan ke DataStore

UserPreferencesRepository saat ini hanya mengekspos cara sinkron untuk menetapkan flag tata urutan dan memiliki masalah serentak. Kita akan mengekspos dua metode untuk memperbarui tata urutan: enableSortByDeadline() dan enableSortByPriority(); keduanya bergantung pada nilai tata urutan saat ini. Namun, jika salah satu metode dipanggil sebelum metode lainnya selesai, nilai akhir akan salah.

Karena DataStore menjamin update data terjadi secara transaksional, kita tidak akan mengalami masalah ini lagi. Mari melakukan perubahan berikut:

  • Perbarui enableSortByDeadline() dan enableSortByPriority() agar menjadi fungsi suspend yang menggunakan dataStore.edit().
  • Pada blok transformasi edit(), kita akan mendapatkan currentOrder dari parameter Preferences, bukan mengambilnya dari kolom _sortOrderFlow.
  • Daripada memanggil updateSortOrder(newSortOrder), kita dapat langsung memperbarui tata urutan dalam preferensi.

Implementasinya akan terlihat seperti berikut ini.

suspend fun enableSortByDeadline(enable: Boolean) {
    // edit handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.edit { preferences ->
        // Get the current SortOrder as an enum
        val currentOrder = SortOrder.valueOf(
            preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
        )

        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_PRIORITY) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_DEADLINE
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_PRIORITY
                } else {
                    SortOrder.NONE
                }
            }
        preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
    }
}

suspend fun enableSortByPriority(enable: Boolean) {
    // edit handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.edit { preferences ->
        // Get the current SortOrder as an enum
        val currentOrder = SortOrder.valueOf(
            preferences[PreferencesKeys.SORT_ORDER] ?: SortOrder.NONE.name
        )

        val newSortOrder =
            if (enable) {
                if (currentOrder == SortOrder.BY_DEADLINE) {
                    SortOrder.BY_DEADLINE_AND_PRIORITY
                } else {
                    SortOrder.BY_PRIORITY
                }
            } else {
                if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
                    SortOrder.BY_DEADLINE
                } else {
                    SortOrder.NONE
                }
            }
        preferences[PreferencesKeys.SORT_ORDER] = newSortOrder.name
    }
}

Setelah UserPreferencesRepository menyimpan flag tampilkan yang telah selesai dan tata urutan di DataStore dan mengekspos Flow<UserPreferences>, perbarui TasksViewModel untuk menggunakannya.

Hapus showCompletedFlow dan sortOrderFlow, lalu buat nilai yang disebut userPreferencesFlow yang diinisialisasi dengan userPreferencesRepository.userPreferencesFlow:

private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow

Pada pembuatan tasksUiModelFlow, ganti showCompletedFlow dan sortOrderFlow dengan userPreferencesFlow. Ganti parameter yang sesuai.

Saat memanggil filterSortTasks, teruskan showCompleted dan sortOrder dari userPreferences. Kode akan terlihat seperti berikut:

private val tasksUiModelFlow = combine(
        repository.tasks,
        userPreferencesFlow
    ) { tasks: List<Task>, userPreferences: UserPreferences ->
        return@combine TasksUiModel(
            tasks = filterSortTasks(
                tasks,
                userPreferences.showCompleted,
                userPreferences.sortOrder
            ),
            showCompleted = userPreferences.showCompleted,
            sortOrder = userPreferences.sortOrder
        )
    }

Fungsi showCompletedTasks() kini harus diperbarui agar dapat memanggil userPreferencesRepository.updateShowCompleted(). Karena ini adalah fungsi penangguhan, buat coroutine baru dalam viewModelScope:

fun showCompletedTasks(show: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.updateShowCompleted(show)
    }
}

Fungsi userPreferencesRepository enableSortByDeadline() dan enableSortByPriority() kini merupakan fungsi penangguhan sehingga juga harus dipanggil dalam coroutine baru yang diluncurkan di viewModelScope:

fun enableSortByDeadline(enable: Boolean) {
    viewModelScope.launch {
       userPreferencesRepository.enableSortByDeadline(enable)
    }
}

fun enableSortByPriority(enable: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.enableSortByPriority(enable)
    }
}

Membersihkan UserPreferencesRepository

Mari kita hapus kolom dan metode yang tidak diperlukan lagi. Anda dapat menghapus hal-hal berikut:

  • _sortOrderFlow
  • sortOrderFlow
  • updateSortOrder()
  • private val sortOrder: SortOrder

Aplikasi sekarang telah berhasil dikompilasi. Mari kita jalankan untuk melihat apakah flag tampilkan yang telah selesai dan tata urutan disimpan dengan benar.

Lihat cabang preferensi di repositori codelab untuk membandingkan perubahan.

Setelah bermigrasi ke Preferences DataStore, mari kita rangkum apa yang telah kita pelajari:

  • SharedPreferences memiliki serangkaian kelemahan - mulai dari API sinkron yang bisa terlihat aman untuk dipanggil di UI thread, tidak ada mekanisme peringatan error, kurangnya API transaksional, dan lainnya.
  • DataStore adalah pengganti SharedPreferences yang mengatasi sebagian besar kekurangan API.
  • DataStore memiliki API asinkron sepenuhnya yang menggunakan coroutine Kotlin dan Flow, menangani migrasi data, menjamin konsistensi data, dan menangani kerusakan data.