Menangani Proto DataStore

1. Pengantar

Apa itu DataStore?

DataStore adalah solusi penyimpanan data yang baru dan lebih baik yang ditujukan untuk menggantikan SharedPreferences. Dibuat di Flow dan coroutine Kotlin, DataStore menyediakan dua implementasi yang berbeda: Proto DataStore, yang memungkinkan menyimpan objek yang diketik (didukung oleh buffering protokol) dan Preference 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 Preference DataStore dan Proto DataStore, serta keuntungan dari keduanya.
  • Cara menggunakan Proto DataStore.
  • Cara bermigrasi dari SharedPreferences ke Proto 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 disimpan ke disk menggunakan objek SharedPreferences.

Karena DataStore memiliki dua implementasi yang berbeda: Preference DataStore dan Proto DataStore, Anda akan mempelajari cara menggunakan Proto DataStore untuk menyelesaikan tugas berikut dalam setiap penerapan:

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

Sebaiknya gunakan juga codelab Preference DataStore sehingga Anda memahami perbedaan antara keduanya dengan lebih baik.

Yang akan Anda butuhkan

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

2. Mempersiapkan

Pada langkah ini, Anda akan mendownload kode untuk seluruh codelab kemudian menjalankan aplikasi contoh sederhana.

Untuk memulainya secepat mungkin, kami telah menyiapkan project awal 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 proto_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 Arctic Fox.
  2. Jalankan aplikasi yang menjalankan konfigurasi di perangkat atau emulator.

b3c0dfdb92dfed77.png

Aplikasi mulai berjalan dan menampilkan daftar tugas:

d3972939a2de88ba.png

3. Ringkasan project

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. Untuk alasan kemudahan, class ini menampilkan data hardcode dan mengeksposnya melalui Flow untuk merepresentasikan 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 memperlihatkan 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 diperlukan untuk mem-build data yang perlu ditampilkan di UI: daftar tugas, flag showCompleted dan sortOrder, 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 menyimpan flag showCompleted terbaru, 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.

Ada beberapa masalah dengan kode:

  • Kita memblokir UI thread pada I/O disk saat menginisialisasi UserPreferencesRepository.sortOrder. Hal ini dapat mengakibatkan jank pada UI.
  • Flag showCompleted 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 showCompleted dan sortOrder 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.

4. DataStore - dasar-dasar

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 dan RxJava 2 & 3 Flowable)

✅ (melalui Flow dan RxJava 2 & 3 Flowable)

API sinkron

✅ (tetapi tidak aman untuk dipanggil di UI thread)

Aman untuk dipanggil di UI thread

❌(1)

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

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

Dapat memperingatkan adanya error

Aman dari pengecualian runtime

❌(2)

Memiliki API transaksional dengan jaminan konsistensi kuat

Menangani migrasi data

Keamanan jenis

✅ dengan Buffering Protokol

(1) 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.

(2) SharedPreferences menampilkan error penguraian sebagai pengecualian runtime.

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

5. Proto DataStore - ringkasan

Salah satu kelemahan dari SharedPreferences dan Preference DataStore adalah tidak ada cara untuk menentukan skema atau memastikan bahwa kunci diakses menggunakan jenis yang benar. Proto DataStore mengatasi masalah ini menggunakan Buffering protokol untuk menentukan skema. Dengan menggunakan proto, DataStore tahu jenis yang disimpan dan akan menyediakannya, sehingga tidak perlu menggunakan kunci.

Mari kita lihat cara menambahkan Proto DataStore dan Protobuf ke project, pengertian dari Buffering protokol, dan cara menggunakannya dengan Proto DataStore serta cara memigrasikan SharedPreferences ke DataStore.

Menambahkan dependensi

Untuk menggunakan Proto DataStore dan mendapatkan Protobuf untuk membuat kode skema, beberapa perubahan pada file build.gradle harus dibuat:

  • Menambahkan plugin Protobuf
  • Menambahkan dependensi Protobuf dan Proto DataStore
  • Mengonfigurasi Protobuf
plugins {
    ...
    id "com.google.protobuf" version "0.8.17"
}

dependencies {
    implementation  "androidx.datastore:datastore-core:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.18.0"
    ...
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

6. Menentukan dan menggunakan objek protobuf

Buffering protokol adalah mekanisme untuk melakukan serialisasi data terstruktur. Anda menentukan cara pembuatan struktur data satu kali, lalu compiler membuat kode sumber untuk menulis dan membaca data terstruktur dengan mudah.

Membuat file proto

Anda menentukan skema dalam file proto. Dalam codelab, kami memiliki 2 preferensi pengguna: show_completed dan sort_order; saat ini keduanya diwakili sebagai dua objek yang berbeda. Jadi, salah satu sasaran kita adalah menggabungkan dua flag ini dalam class UserPreferences yang disimpan di DataStore. Daripada menentukan class ini di Kotlin, class ini akan didefinisikan dalam skema Protobuf.

Lihat Panduan bahasa proto untuk mengetahui info sintaksis yang lebih lengkap. Dalam codelab ini, kita hanya akan berfokus pada jenis yang dibutuhkan.

Buat file baru bernama user_prefs.proto di direktori app/src/main/proto. Jika Anda tidak melihat struktur folder ini, beralih ke Tampilan project. Dalam Protobuf, setiap struktur ditentukan menggunakan kata kunci message dan setiap anggota struktur ditentukan di dalam pesan, berdasarkan jenis dan nama, dan akan diberi urutan berdasarkan angka 1. Mari kita tentukan pesan UserPreferences yang, untuk saat ini, hanya memiliki nilai (logika) boolean yang disebut show_completed.

syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;
}

Membuat penserialisasi

Untuk memberi tahu DataStore cara membaca dan menulis jenis data yang telah ditentukan dalam file proto, kita perlu menerapkan Penserialisasi. Penserialisasi juga menentukan nilai default yang akan ditampilkan jika tidak ada data pada disk. Buat file baru bernama UserPreferencesSerializer dalam paket data:

object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    override suspend fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}

7. Menyimpan data di Proto DataStore

Membuat DataStore

Flag showCompleted disimpan dalam memori di TasksViewModel, tetapi flag harus disimpan dalam UserPreferencesRepository di instance DataStore.

Untuk membuat instance DataStore, kita menggunakan delegasi dataStore, dengan Context sebagai penerima. Delegasi ini memiliki dua parameter wajib:

  • Nama file yang akan ditindaklanjuti oleh DataStore.
  • Penserialisasi untuk jenis yang digunakan dengan DataStore. Dalam kasus kami: UserPreferencesSerializer.

Dalam codelab ini, mari lakukan hal ini di TasksActivity agar lebih praktis:

private const val USER_PREFERENCES_NAME = "user_preferences"
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
private const val SORT_ORDER_KEY = "sort_order"

private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer
)

Delegasi dataStore memastikan bahwa kita memiliki satu instance DataStore dengan nama tersebut di aplikasi kita. Saat ini, UserPreferencesRepository diimplementasikan sebagai singleton, karena dapat mempertahankan sortOrderFlow dan menghindari terpengaruh siklus proses TasksActivity. Karena UserPreferenceRepository akan kompatibel dengan data dari DataStore serta tidak akan membuat dan mempertahankan objek baru apa pun, kita dapat menghapus implementasi singleton:

  • Hapus companion object
  • Setel constructor menjadi publik

UserPreferencesRepository harus mendapatkan instance DataStore sebagai parameter konstruktor. Untuk saat ini, kita dapat menetapkan Context sebagai parameter karena dibutuhkan oleh SharedPreferences, tetapi kita akan menghapusnya nanti.

class UserPreferencesRepository(
    private val userPreferencesStore: DataStore<UserPreferences>,
    context: Context
) { ... }

Mari kita memperbarui konstruksi UserPreferencesRepository di TasksActivity dan memasukkan dataStore:

viewModel = ViewModelProvider(
    this,
    TasksViewModelFactory(
        TasksRepository,
        UserPreferencesRepository(dataStore, this)
    )
).get(TasksViewModel::class.java)

Membaca data dari Proto DataStore

Proto DataStore mengekspos data yang tersimpan di Flow<UserPreferences>. Mari kita buat nilai userPreferencesFlow: Flow<UserPreferences> publik yang ditetapkan ke dataStore.data:

val userPreferencesFlow: Flow<UserPreferences> = dataStore.data

Menangani pengecualian saat membaca data

Saat DataStore membaca data dari file, IOException akan muncul saat terjadi error selama pembacaan data. Kita dapat mengatasinya dengan menggunakan transformasi Flow catch dan hanya mencatat error:

private val TAG: String = "UserPreferencesRepo"

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) {
            Log.e(TAG, "Error reading sort order preferences.", exception)
            emit(UserPreferences.getDefaultInstance())
        } else {
            throw exception
        }
    }

Menulis data ke Proto DataStore

Untuk menulis data, DataStore menawarkan fungsi DataStore.updateData() yang ditangguhkan dengan parameter status saat ini dari UserPreferences. Untuk mengupdatenya, kita harus mengubah objek preferensi menjadi builder, menetapkan nilai baru, lalu membuat preferensi baru.

updateData() mengupdate data secara transaksional dalam operasi baca-tulis-modifikasi yang sederhana. Coroutine akan selesai setelah data disimpan di disk.

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

suspend fun updateShowCompleted(completed: Boolean) {
    dataStore.updateData { preferences ->
        preferences.toBuilder().setShowCompleted(completed).build()
    }
}

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

8. SharedPreferences ke Proto DataStore

Menentukan data yang akan disimpan di proto

Tata urutan disimpan di SharedPreferences. Mari kita pindahkan ke DataStore. Untuk melakukannya, mulai dengan mengupdate UserPreferences di file proto untuk menyimpan tata urutan. Karena SortOrder adalah enum, kita harus menentukannya di UserPreference. enums ditentukan dalam protobuf yang mirip dengan Kotlin.

Untuk enumerasi, nilai default adalah nilai pertama yang tercantum dalam definisi jenis enum. Namun, saat bermigrasi dari SharedPreferences, kita perlu mengetahui apakah nilai yang telah didapatkan adalah nilai default atau nilai yang sebelumnya ditetapkan di SharedPreferences. Untuk memudahkannya, kami menetapkan nilai baru ke enum SortOrder: UNSPECIFIED dan mencantumkannya terlebih dahulu sehingga nilai baru ini dapat menjadi nilai default.

File user_prefs.proto akan terlihat seperti ini:

syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;

  // defines tasks sorting order: no order, by deadline, by priority, by deadline and priority
  enum SortOrder {
    UNSPECIFIED = 0;
    NONE = 1;
    BY_DEADLINE = 2;
    BY_PRIORITY = 3;
    BY_DEADLINE_AND_PRIORITY = 4;
  }

  // user selected tasks sorting order
  SortOrder sort_order = 2;
}

Bersihkan dan lakukan build ulang pada project Anda untuk memastikan bahwa objek UserPreferences baru telah dibuat dan berisi kolom baru.

Setelah SortOrder ditetapkan dalam file proto, kita dapat menghapus deklarasi dari UserPreferencesRepository. Hapus:

enum class SortOrder {
    NONE,
    BY_DEADLINE,
    BY_PRIORITY,
    BY_DEADLINE_AND_PRIORITY
}

Pastikan impor SortOrder yang tepat digunakan di mana pun:

import com.codelab.android.datastore.UserPreferences.SortOrder

Di TasksViewModel.filterSortTasks(), kami melakukan berbagai tindakan berdasarkan jenis SortOrder. Setelah menambahkan opsi UNSPECIFIED, kita harus menambahkan kasus lain untuk pernyataan when(sortOrder). Karena kami tidak ingin menangani opsi lain selain yang ada saat ini, kami dapat memberikan UnsupportedOperationException dalam kasus lain.

Fungsi filterSortTasks() terlihat seperti ini:

private fun filterSortTasks(
    tasks: List<Task>,
    showCompleted: Boolean,
    sortOrder: SortOrder
): List<Task> {
    // filter the tasks
    val filteredTasks = if (showCompleted) {
        tasks
    } else {
        tasks.filter { !it.completed }
    }
    // sort the tasks
    return when (sortOrder) {
        SortOrder.UNSPECIFIED -> filteredTasks
        SortOrder.NONE -> filteredTasks
        SortOrder.BY_DEADLINE -> filteredTasks.sortedByDescending { it.deadline }
        SortOrder.BY_PRIORITY -> filteredTasks.sortedBy { it.priority }
        SortOrder.BY_DEADLINE_AND_PRIORITY -> filteredTasks.sortedWith(
            compareByDescending<Task> { it.deadline }.thenBy { it.priority }
        )
        // We shouldn't get any other values
        else -> throw UnsupportedOperationException("$sortOrder not supported")
    }
}

Bermigrasi dari SharedPreferences

Untuk membantu migrasi, DataStore menentukan class SharedPreferencesMigration. Metode by dataStore yang membuat DataStore (digunakan di TasksActivity), juga mengekspos parameter produceMigrations. Dalam blok ini, kita membuat daftar DataMigration yang harus dijalankan untuk instance DataStore ini. Dalam hal ini, kita hanya memiliki satu migrasi: SharedPreferencesMigration.

Saat mengimplementasikan SharedPreferencesMigration, blok migrate menyediakan dua parameter:

  • SharedPreferencesView yang memungkinkan data untuk diambil dari SharedPreferences
  • Data UserPreferences saat ini

Objek UserPreferences harus ditampilkan.

Saat menerapkan blok migrate, lakukan langkah-langkah berikut:

  1. Periksa nilai sortOrder di UserPreferences.
  2. Jika ini adalah SortOrder.UNSPECIFIED, artinya nilai harus diambil dari SharedPreferences. Jika SortOrder tidak ada, SortOrder.NONE dapat digunakan sebagai default.
  3. Setelah mendapatkan urutan penyortiran, kita harus mengonversi objek UserPreferences menjadi builder, menetapkan tata urutan, lalu membuat objek lagi dengan memanggil build(). Tidak ada kolom lain yang akan terpengaruh dengan perubahan ini.
  4. Jika nilai sortOrder di UserPreferences bukanlah SortOrder.UNSPECIFIED, data saat ini yang dapat ditampilkan hanyalah data yang telah didapatkan di migrate karena migrasi pasti sudah berjalan dengan baik.
private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer,
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context,
                USER_PREFERENCES_NAME
            ) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
                // Define the mapping from SharedPreferences to UserPreferences
                if (currentData.sortOrder == SortOrder.UNSPECIFIED) {
                    currentData.toBuilder().setSortOrder(
                        SortOrder.valueOf(
                            sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!!
                        )
                    ).build()
                } else {
                    currentData
                }
            }
        )
    }
)

Setelah menentukan logika migrasi, selanjutnya DataStore harus diberi tahu bahwa data tersebut harus digunakan. Untuk itu, update builder DataStore dan tetapkan daftar baru ke parameter migrations yang berisi instance SharedPreferencesMigration:

private val dataStore: DataStore<UserPreferences> = context.createDataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer,
    migrations = listOf(sharedPrefsMigration)
)

Menyimpan urutan penyortiran ke DataStore

Untuk mengupdate urutan penyortiran jika enableSortByDeadline() dan enableSortByPriority() dipanggil, lakukan hal berikut:

  • Panggil fungsi masing-masing di lambda dataStore.updateData().
  • Karena updateData() adalah fungsi penangguhan, enableSortByDeadline() dan enableSortByPriority() juga harus dibuat sebagai fungsi penangguhan.
  • Gunakan UserPreferences saat ini yang diterima dari updateData() untuk membuat urutan penyortiran baru
  • Update UserPreferences dengan mengonversinya menjadi builder, menetapkan urutan penyortiran baru, lalu melakukan update lagi pada preferensi.

Penerapan enableSortByDeadline() akan terlihat seperti ini. Anda dapat melakukan perubahan untuk enableSortByPriority() sendiri.

suspend fun enableSortByDeadline(enable: Boolean) {
    // updateData handles data transactionally, ensuring that if the sort is updated at the same
    // time from another thread, we won't have conflicts
    dataStore.updateData { preferences ->
        val currentOrder = preferences.sortOrder
        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.toBuilder().setSortOrder(newSortOrder).build()
    }
}

Anda sekarang dapat menghapus parameter konstruktor context dan semua penggunaan SharedPreferences.

9. Memperbarui TasksViewModel untuk menggunakan UserPreferencesRepository

Setelah UserPreferencesRepository menyimpan flag show_completed dan sort_order 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
  • private val sharedPreferences

Aplikasi sekarang telah berhasil dikompilasi. Mari kita jalankan untuk melihat apakah flag show_completed dan sort_order disimpan dengan benar.

Lihat cabang proto_datastore di repo codelab untuk membandingkan perubahan.

10. Rangkuman

Setelah bermigrasi ke Proto DataStore, mari kita rangkum semua hal yang telah dipelajari:

  • SharedPreferences memiliki serangkaian kelemahan, mulai dari API sinkron yang bisa terlihat aman untuk dipanggil di UI thread, tidak ada mekanisme pembuatan sinyal 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.