Menyimpan preferensi secara lokal dengan DataStore

1. Sebelum memulai

Pengantar

Di unit ini, Anda telah mempelajari cara menggunakan SQL dan Room untuk menyimpan data secara lokal di perangkat. SQL dan Room adalah alat yang canggih. Namun, jika Anda tidak memerlukan penyimpanan data relasional, DataStore dapat memberikan solusi sederhana. Komponen DataStore Jetpack adalah cara yang bagus untuk menyimpan set data kecil dan sederhana dengan overhead rendah. DataStore memiliki dua implementasi yang berbeda, yaitu Preferences DataStore dan Proto DataStore.

  • Preferences DataStore menyimpan key-value pair. Nilainya dapat berupa jenis data dasar Kotlin, seperti String, Boolean, dan Integer. Database ini tidak menyimpan set data yang kompleks. Tidak memerlukan skema yang telah ditetapkan sebelumnya. Kasus penggunaan utama Preferences Datastore adalah untuk menyimpan preferensi pengguna di perangkatnya.
  • Proto DataStore menyimpan jenis data kustom. Implementasi ini memerlukan skema yang telah ditentukan sebelumnya dan memetakan definisi proto dengan struktur objek.

Hanya Preferences DataStore yang dibahas dalam codelab ini, tetapi Anda dapat membaca Proto DataStore lebih lanjut di dokumentasi DataStore.

Preferences DataStore adalah cara yang tepat untuk menyimpan setelan yang dikontrol pengguna. Dalam codelab ini, Anda akan mempelajari cara menerapkan DataStore untuk melakukannya.

Prasyarat:

Yang akan Anda butuhkan

  • Komputer yang memiliki akses internet dan Android Studio
  • Perangkat atau emulator
  • Kode awal untuk aplikasi Dessert Release

Yang akan Anda build

Aplikasi Dessert Release menampilkan daftar rilis Android. Ikon di panel aplikasi mengalihkan tata letak antara tampilan petak dan tampilan daftar.

7f93523c9844380f.png 5b542bebfbd7a3a1.png

Dalam status saat ini, aplikasi tidak mempertahankan pemilihan tata letak. Saat Anda menutup aplikasi, pilihan tata letak tidak akan disimpan dan setelan akan kembali ke pilihan default. Dalam codelab ini, Anda akan menambahkan DataStore ke aplikasi Dessert Release dan menggunakannya untuk menyimpan preferensi pemilihan tata letak.

2. Mendownload kode awal

Klik link berikut guna mendownload semua kode untuk codelab ini:

Atau jika mau, Anda dapat meng-clone kode Dessert Release dari GitHub:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout starter
  1. Di Android Studio, buka folder basic-android-kotlin-compose-training-dessert-release.
  2. Buka kode aplikasi Dessert Release di Android Studio.

3. Menyiapkan dependensi

Tambahkan kode berikut ke dependencies di file app/build.gradle.kts:

implementation("androidx.datastore:datastore-preferences:1.0.0")

4. Mengimplementasikan repositori preferensi pengguna

  1. Pada paket data, buat class baru bernama UserPreferencesRepository.

adf3ec7481163f56.png

  1. Di konstruktor UserPreferencesRepository, tentukan properti nilai pribadi untuk mewakili instance objek DataStore dengan jenis Preferences.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore menyimpan key-value pair. Untuk mengakses nilai, Anda harus menentukan kunci.

  1. Buat companion object di dalam class UserPreferencesRepository.
  2. Gunakan fungsi booleanPreferencesKey() untuk menentukan kunci dan teruskan nama is_linear_layout. Serupa dengan nama tabel SQL, kunci perlu menggunakan format garis bawah. Kunci ini digunakan untuk mengakses nilai boolean yang menunjukkan apakah tata letak linear harus ditampilkan.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
    private companion object {
        val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    }
    ...
}

Menulis ke DataStore

Anda membuat dan mengubah nilai dalam DataStore dengan meneruskan lambda ke metode edit(). Lambda meneruskan instance MutablePreferences, yang dapat Anda gunakan untuk memperbarui nilai di DataStore. Semua update di dalam lambda ini dijalankan sebagai satu transaksi. Dengan kata lain, update bersifat atomik — semuanya terjadi dalam satu waktu. Jenis update ini mencegah situasi saat beberapa nilai diperbarui, sedangkan yang lainnya tidak.

  1. Buat fungsi penangguhan, lalu beri nama saveLayoutPreference().
  2. Dalam fungsi saveLayoutPreference(), panggil metode edit() pada objek dataStore.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. Agar kode lebih mudah dibaca, tentukan nama untuk MutablePreferences yang disediakan di isi lambda. Gunakan properti tersebut untuk menyetel nilai dengan kunci yang Anda tentukan dan boolean akan diteruskan ke fungsi saveLayoutPreference().
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}

Membaca dari DataStore

Setelah Anda membuat cara untuk menulis isLinearLayout ke dalam dataStore, lakukan langkah-langkah berikut untuk membacanya:

  1. Buat properti di UserPreferencesRepository dari jenis Flow<Boolean> yang disebut isLinearLayout.
val isLinearLayout: Flow<Boolean> =
  1. Anda dapat menggunakan properti DataStore.data untuk menampilkan nilai DataStore. Setel isLinearLayout ke properti data objek DataStore.
val isLinearLayout: Flow<Boolean> = dataStore.data

Properti data adalah Flow dari objek Preferences. Objek Preferences berisi semua key-value pair di DataStore. Setiap kali data di DataStore diperbarui, objek Preferences baru akan dimunculkan ke dalam Flow.

  1. Gunakan fungsi peta untuk mengonversi Flow<Preferences> menjadi Flow<Boolean>.

Fungsi ini menerima lambda dengan objek Preferences saat ini sebagai parameter. Anda dapat menetapkan kunci yang telah ditentukan sebelumnya untuk mendapatkan preferensi tata letak. Perlu diingat bahwa nilai mungkin tidak ada jika saveLayoutPreference belum dipanggil, jadi Anda juga harus menyediakan nilai default.

  1. Tentukan true untuk menyetel default ke tampilan tata letak linear.
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

Penanganan pengecualian

Setiap kali Anda berinteraksi dengan sistem file di perangkat, mungkin saja ada yang gagal. Misalnya, file mungkin tidak ada, atau disk penuh atau dilepas. Saat DataStore membaca dan menulis data dari file, IOExceptions dapat terjadi saat mengakses DataStore. Anda menggunakan operator catch{} untuk menangkap pengecualian dan menangani kegagalan ini.

  1. Di objek pendamping, terapkan properti string TAG yang tidak dapat diubah dan digunakan untuk logging.
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
  1. Preferences DataStore menampilkan IOException ketika terjadi error saat membaca data. Di blok inisialisasi isLinearLayout, sebelum map(), gunakan operator catch{} untuk menangkap IOException.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. Di blok catch, jika ada IOexception, catat error dan buat emptyPreferences(). Jika jenis pengecualian lain ditampilkan, sebaiknya tampilkan kembali pengecualian tersebut. Dengan memunculkan emptyPreferences() saat terjadi error, fungsi peta masih dapat dipetakan ke nilai default.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {
        if(it is IOException) {
            Log.e(TAG, "Error reading preferences.", it)
            emit(emptyPreferences())
        } else {
            throw it
        }
    }
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }

5. Melakukan inisialisasi DataStore

Dalam codelab ini, Anda harus menangani injeksi dependensi secara manual. Oleh karena itu, Anda harus menyediakan class UserPreferencesRepository dengan Preferences DataStore secara manual. Ikuti langkah-langkah berikut untuk memasukkan DataStore ke dalam UserPreferencesRepository.

  1. Temukan paket dessertrelease.
  2. Dalam direktori ini, buat class baru bernama DessertReleaseApplication dan implementasikan class Application. Ini adalah container untuk DataStore Anda.
class DessertReleaseApplication: Application() {
}
  1. Di dalam file DessertReleaseApplication.kt, tetapi di luar class DessertReleaseApplication, deklarasikan private const val yang disebut LAYOUT_PREFERENCE_NAME.
  2. Tetapkan variabel LAYOUT_PREFERENCE_NAME nilai string layout_preferences, yang kemudian dapat Anda gunakan sebagai nama Preferences Datastore yang Anda buat instance-nya di langkah berikutnya.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. Masih berada di luar isi class DessertReleaseApplication, tetapi dalam file DessertReleaseApplication.kt, buat properti nilai pribadi jenis DataStore<Preferences> yang disebut Context.dataStore menggunakan delegasi preferencesDataStore. Teruskan LAYOUT_PREFERENCE_NAME untuk parameter name dari delegasi preferencesDataStore.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)
  1. Di dalam isi class DessertReleaseApplication, buat instance lateinit var dari UserPreferencesRepository.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository
}
  1. Ganti metode onCreate().
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
    }
}
  1. Di dalam metode onCreate(), lakukan inisialisasi userPreferencesRepository dengan membuat UserPreferencesRepository dengan dataStore sebagai parameternya.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
        userPreferencesRepository = UserPreferencesRepository(dataStore)
    }
}
  1. Tambahkan baris berikut di dalam tag <application> dalam file AndroidManifest.xml.
<application
    android:name=".DessertReleaseApplication"
    ...
</application>

Pendekatan ini menentukan class DessertReleaseApplication sebagai titik entri aplikasi. Tujuan kode ini adalah untuk melakukan inisialisasi dependensi yang ditentukan di class DessertReleaseApplication sebelum meluncurkan MainActivity.

6. Menggunakan UserPreferencesRepository

Menyediakan repositori untuk ViewModel

Setelah UserPreferencesRepository tersedia melalui injeksi dependensi, Anda dapat menggunakannya di DessertReleaseViewModel.

  1. Di DessertReleaseViewModel, buat properti UserPreferencesRepository sebagai parameter konstruktor.
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. Dalam objek pendamping ViewModel, di blok viewModelFactory initializer, dapatkan instance DessertReleaseApplication menggunakan kode berikut.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. Buat instance DessertReleaseViewModel dan teruskan userPreferencesRepository.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}

UserPreferencesRepository kini dapat diakses oleh ViewModel. Langkah berikutnya adalah menggunakan kemampuan baca dan tulis UserPreferencesRepository yang Anda implementasikan sebelumnya.

Menyimpan preferensi tata letak

  1. Edit fungsi selectLayout() di DessertReleaseViewModel untuk mengakses repositori preferensi dan memperbarui preferensi tata letak.
  2. Ingatlah bahwa penulisan ke DataStore dilakukan secara asinkron dengan fungsi suspend. Mulai Coroutine baru untuk memanggil fungsi saveLayoutPreference() repositori preferensi.
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}

Membaca preferensi tata letak

Di bagian ini, Anda akan memfaktorkan ulang uiState: StateFlow yang ada di ViewModel untuk mencerminkan isLinearLayout: Flow dari repositori.

  1. Hapus kode yang melakukan inisialisasi properti uiState ke MutableStateFlow(DessertReleaseUiState).
val uiState: StateFlow<DessertReleaseUiState> =

Preferensi tata letak linear dari repositori memiliki dua kemungkinan nilai, yaitu benar atau salah, dalam bentuk Flow<Boolean>. Nilai ini harus dipetakan ke status UI.

  1. Setel StateFlow ke hasil transformasi koleksi map() yang dipanggil di isLinearLayout Flow.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. Tampilkan instance class data DessertReleaseUiState, dengan meneruskan isLinearLayout Boolean. Layar menggunakan status UI ini untuk menentukan string dan ikon yang benar untuk ditampilkan.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }

UserPreferencesRepository.isLinearLayout adalah Flow yang bersifat cold. Namun, untuk memberikan status ke UI, sebaiknya gunakan hot flow, seperti StateFlow, sehingga status selalu tersedia langsung ke UI.

  1. Gunakan fungsi stateIn() untuk mengonversi Flow menjadi StateFlow.
  2. Fungsi stateIn() menerima tiga parameter: scope, started, dan initialValue. Teruskan viewModelScope, SharingStarted.WhileSubscribed(5_000), dan DessertReleaseUiState() untuk parameter ini.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = DessertReleaseUiState()
    )
  1. Luncurkan aplikasi. Perhatikan bahwa Anda dapat mengklik ikon beralih untuk beralih antara tata letak petak dan tata letak linear.

7f93523c9844380f.png 5b542bebfbd7a3a1.png

Selamat! Anda berhasil menambahkan Preferences DataStore ke aplikasi untuk menyimpan preferensi tata letak pengguna.

7. Mendapatkan kode solusi

Untuk mendownload kode codelab yang sudah selesai, Anda dapat menggunakan perintah git berikut:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout main

Atau, Anda dapat mendownload repositori sebagai file ZIP, lalu mengekstraknya, dan membukanya di Android Studio.

Jika Anda ingin melihat kode solusi, lihat di GitHub.