Mempertahankan data dengan Room

1. Sebelum memulai

Sebagian besar aplikasi berkualitas produksi memiliki data yang harus dipertahankan oleh aplikasi. Misalnya, aplikasi dapat menyimpan playlist lagu, item dalam daftar tugas, catatan pengeluaran dan pendapatan, katalog konstelasi, atau histori data pribadi. Untuk kasus penggunaan semacam itu, Anda menggunakan database guna menyimpan data persisten ini.

Room adalah library persistensi yang merupakan bagian dari Android Jetpack. Room adalah lapisan abstraksi di atas database SQLite. SQLite menggunakan bahasa khusus (SQL) untuk menjalankan operasi database. Dibandingkan menggunakan SQLite secara langsung, Room lebih menyederhanakan tugas-tugas penyiapan, konfigurasi, dan interaksi database dengan aplikasi. Room juga menyediakan pemeriksaan waktu kompilasi terhadap pernyataan SQLite.

Lapisan abstraksi adalah sekumpulan fungsi yang menyembunyikan implementasi/kompleksitas yang mendasarinya. Fungsi ini menyediakan antarmuka ke kumpulan fungsi yang ada, seperti SQLite dalam hal ini.

Gambar di bawah menunjukkan kesesuaian Room sebagai sumber data dengan arsitektur keseluruhan yang direkomendasikan dalam kursus ini. Room adalah Sumber Data.

lapisan data berisi repositori dan sumber data

Prasyarat

  • Kemampuan untuk membangun antarmuka pengguna (UI) dasar untuk aplikasi Android menggunakan Jetpack Compose.
  • Kemampuan untuk menggunakan composable seperti Text, Icon, IconButton, dan LazyColumn.
  • Kemampuan untuk menggunakan composable NavHost untuk menentukan rute dan layar di aplikasi Anda.
  • Kemampuan menavigasi antarlayar menggunakan NavHostController.
  • Pemahaman tentang komponen arsitektur Android ViewModel. Kemampuan menggunakan ViewModelProvider.Factory untuk membuat instance ViewModels.
  • Pemahaman tentang dasar-dasar konkurensi.
  • Kemampuan menggunakan coroutine untuk tugas yang berjalan lama.
  • Pengetahuan dasar tentang database SQLite dan bahasa SQL.

Yang akan Anda pelajari

  • Cara membuat dan berinteraksi dengan database SQLite menggunakan library Room.
  • Cara membuat entity, objek akses data (DAO), dan class database.
  • Cara menggunakan DAO untuk memetakan fungsi Kotlin ke kueri SQL.

Yang akan Anda bangun

  • Anda akan membangun aplikasi Inventory yang menyimpan item inventaris ke dalam database SQLite.

Yang Anda perlukan

  • Kode awal untuk aplikasi Inventory
  • Komputer dengan Android Studio
  • Perangkat atau emulator dengan API level 26 atau yang lebih tinggi

2. Ringkasan aplikasi

Dalam codelab ini, Anda akan menggunakan kode awal aplikasi Inventory dan menambahkan lapisan database ke dalamnya menggunakan library Room. Versi final aplikasi menampilkan daftar item dari database inventaris. Pengguna memiliki opsi untuk menambahkan item baru, mengupdate item yang ada, dan menghapus item dari database inventaris. Untuk codelab ini, Anda menyimpan data item ke database Room. Anda akan menyelesaikan fungsi aplikasi lainnya di codelab berikutnya.

Layar ponsel dengan item inventaris

Tambahkan layar item yang ditampilkan di layar ponsel.

Layar Tambahkan Item dengan detail item yang terisi.

3. Ringkasan aplikasi awal

Mendownload kode awal untuk codelab ini

Untuk memulai, download kode awal:

Atau, Anda dapat membuat clone repositori GitHub untuk kode tersebut:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout starter

Anda dapat menjelajahi kode di repositori GitHub Inventory app.

Ringkasan kode awal

  1. Buka project dengan kode awal di Android Studio.
  2. Jalankan aplikasi di perangkat Android atau di emulator. Pastikan emulator atau perangkat yang terhubung berjalan dengan API level 26 atau lebih tinggi. Database Inspector berfungsi di emulator/perangkat yang menjalankan API level 26 dan versi lebih baru.
  1. Perhatikan bahwa aplikasi tidak menampilkan data inventaris.
  2. Ketuk tombol tindakan mengambang (FAB), yang memungkinkan Anda menambahkan item baru ke database.

Aplikasi akan membuka layar baru tempat Anda dapat memasukkan detail item baru.

Inventaris kosong layar ponsel

Tambahkan layar item yang ditampilkan di layar ponsel.

Masalah dengan kode awal

  1. Di layar Add Item, masukkan detail item seperti nama, harga, dan jumlah Item.
  2. Ketuk Simpan. Layar Add Item tidak ditutup, tetapi Anda dapat kembali menggunakan tombol kembali. Fungsi simpan tidak diterapkan sehingga detail item tidak disimpan.

Perhatikan bahwa aplikasi tidak lengkap dan fungsi tombol Save tidak diterapkan.

Layar Tambahkan Item dengan detail item yang terisi.

Dalam codelab ini, Anda akan menambahkan kode yang menggunakan Room untuk menyimpan detail inventaris di database SQLite. Anda menggunakan library persistensi Room untuk berinteraksi dengan database SQLite.

Panduan kode

Kode awal yang Anda download memiliki tata letak layar yang telah didesain sebelumnya untuk Anda. Di jalur ini, Anda akan berfokus untuk menerapkan logika database. Bagian berikut adalah panduan singkat beberapa file untuk membantu Anda memulai.

ui/home/HomeScreen.kt

File ini adalah layar utama, atau layar pertama di aplikasi, yang berisi composable untuk menampilkan daftar inventaris. File ini memiliki FAB + untuk menambahkan item baru ke daftar. Anda nanti akan menampilkan item dalam daftar di jalur tersebut.

Layar ponsel dengan item inventaris

ui/item/ItemEntryScreen.kt

Layar ini mirip dengan ItemEditScreen.kt. Keduanya memiliki kolom teks untuk detail item. Layar ini ditampilkan saat FAB diketuk di layar utama. ItemEntryViewModel.kt adalah ViewModel yang sesuai untuk layar ini.

Layar Tambahkan Item dengan detail item yang terisi.

ui/navigation/InventoryNavGraph.kt

File ini adalah grafik navigasi untuk seluruh aplikasi.

4. Komponen utama Room

Kotlin menyediakan cara mudah untuk menangani data melalui class data. Meskipun mudah untuk bekerja dengan data dalam memori menggunakan class data, untuk mempertahankan data, Anda perlu mengonversi data ini menjadi format yang kompatibel dengan penyimpanan database. Untuk melakukannya, Anda memerlukan tabel untuk menyimpan data dan kueri untuk mengakses dan mengubah data.

Tiga komponen Room berikut membuat alur kerja ini menjadi lancar.

  • Entity Room menampilkan tabel di database aplikasi Anda. Anda menggunakannya untuk memperbarui data yang disimpan dalam baris di tabel dan membuat baris baru untuk penyisipan.
  • DAO Room menyediakan metode yang digunakan aplikasi Anda untuk mengambil, memperbarui, menyisipkan, dan menghapus data dalam database.
  • Class database Room adalah class database yang menyediakan instance DAO yang terkait dengan database tersebut ke aplikasi Anda.

Anda akan menerapkan dan mempelajari komponen ini lebih lanjut nanti di codelab ini. Diagram berikut menunjukkan bagaimana komponen Room bekerja bersama-sama untuk berinteraksi dengan database.

a3288e8f37250031.png

Menambahkan dependensi Room

Dalam tugas ini, Anda akan menambahkan library komponen Room yang diperlukan ke file Gradle Anda.

  1. Buka file gradle level modul build.gradle.kts (Module: InventoryApp.app).
  2. Di blok dependencies, tambahkan dependensi untuk library Room yang ditampilkan dalam kode berikut.
//Room
implementation("androidx.room:room-runtime:${rootProject.extra["room_version"]}")
ksp("androidx.room:room-compiler:${rootProject.extra["room_version"]}")
implementation("androidx.room:room-ktx:${rootProject.extra["room_version"]}")

KSP adalah API yang canggih tetapi sederhana untuk mengurai anotasi Kotlin.

5. Membuat Entity item

Class Entity menentukan tabel, dan setiap instance class ini mewakili baris dalam tabel database. Class entity memiliki pemetaan untuk memberi tahu Room tentang bagaimana class entity bermaksud untuk menampilkan dan berinteraksi dengan informasi dalam database. Di aplikasi Anda, entity menyimpan informasi tentang item inventaris, seperti nama item, harga item, dan jumlah item yang tersedia.

8c9f1659ee82ca43.png

Anotasi @Entity menandai class sebagai class Entity database. Untuk setiap class Entity, aplikasi membuat tabel database untuk menyimpan item. Setiap kolom Entity ditampilkan sebagai kolom dalam database, kecuali jika dinyatakan lain (lihat dokumen Entity untuk detailnya). Setiap instance entity yang disimpan dalam database harus memiliki kunci utama. Kunci utama digunakan untuk mengidentifikasi setiap catatan/entri dalam tabel database Anda secara unik. Setelah aplikasi menetapkan kunci utama, hal itu tidak dapat diubah. Kunci utama merepresentasikan objek entity selama kunci utama itu berada dalam database.

Dalam tugas ini, Anda membuat class Entity dan menentukan kolom untuk menyimpan informasi inventaris berikut untuk setiap item: Int untuk menyimpan kunci utama, String untuk menyimpan nama item, double untuk menyimpan harga item, dan Int untuk menyimpan jumlah yang tersedia.

  1. Buka kode awal di Android Studio.
  2. Buka paket data pada paket dasar com.example.inventory.
  3. Di dalam paket data, buka class Kotlin Item yang mewakili entity database di aplikasi Anda.
// No need to copy over, this is part of the starter code
class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)

Class data

Class data utamanya digunakan untuk menyimpan data di Kotlin. Class data ini ditentukan dengan kata kunci data. Objek class data Kotlin memiliki beberapa manfaat tambahan. Misalnya, compiler otomatis membuat utility untuk membandingkan, mencetak, dan menyalin, seperti toString(), copy(), dan equals().

Contoh:

// Example data class with 2 properties.
data class User(val firstName: String, val lastName: String){
}

Untuk memastikan konsistensi dan perilaku yang bermakna dari kode yang dihasilkan, class data harus memenuhi persyaratan berikut:

  • Konstruktor utama harus memiliki setidaknya satu parameter.
  • Semua parameter konstruktor utama harus berupa val atau var.
  • Class data tidak boleh berupa abstract, open, atau sealed.

Untuk mempelajari class Data lebih lanjut, lihat dokumentasi Class data.

  1. Awali definisi class Item dengan kata kunci data untuk mengonversinya menjadi class data.
data class Item(
    val id: Int,
    val name: String,
    val price: Double,
    val quantity: Int
)
  1. Di atas deklarasi class Item, beri anotasi pada class data dengan @Entity. Gunakan argumen tableName untuk menetapkan items sebagai nama tabel SQLite.
import androidx.room.Entity

@Entity(tableName = "items")
data class Item(
   ...
)
  1. Anotasikan properti id dengan @PrimaryKey untuk menjadikan id sebagai kunci utama. Kunci utama adalah ID untuk mengidentifikasi setiap catatan/entri dalam tabel Item Anda secara unik
import androidx.room.PrimaryKey

@Entity(tableName = "items")
data class Item(
    @PrimaryKey
    val id: Int,
    ...
)
  1. Tetapkan nilai default sebesar 0 pada id, yang diperlukan id untuk membuat nilai id secara otomatis.
  2. Tambahkan parameter autoGenerate ke anotasi @PrimaryKey untuk menentukan apakah kolom kunci utama harus dibuat secara otomatis. Jika autoGenerate disetel ke true, Room akan otomatis membuat nilai unik untuk kolom kunci utama saat instance entity baru dimasukkan ke dalam database. Ini memastikan bahwa setiap instance entity memiliki ID unik, tanpa harus menetapkan nilai ke kolom kunci utama secara manual
data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    // ...
)

Bagus. Setelah membuat class Entity, Anda dapat membuat Objek Akses Data (DAO) untuk mengakses database.

6. Membuat DAO item

Objek Akses Data (DAO) adalah pola yang dapat Anda gunakan untuk memisahkan lapisan persistensi dari bagian aplikasi lainnya dengan menyediakan antarmuka abstrak. Pemisahan ini mengikuti prinsip tanggung jawab tunggal, yang telah Anda lihat di codelab sebelumnya.

Fungsi DAO adalah untuk menyembunyikan semua kerumitan yang terjadi ketika menjalankan operasi database dalam lapisan persistensi yang mendasari, terpisah dari bagian aplikasi lainnya. Hal ini memungkinkan Anda mengubah lapisan data secara terpisah dari kode yang menggunakan data.

8b91b8bbd7256a63.png

Dalam tugas ini, Anda akan menentukan DAO untuk Room. DAO adalah komponen utama Room yang bertanggung jawab untuk menentukan antarmuka yang mengakses database.

DAO yang Anda buat adalah antarmuka kustom yang menyediakan metode praktis untuk melakukan kueri/mengambil, memasukkan, menghapus, dan memperbarui database. Room menghasilkan implementasi dari class ini pada waktu kompilasi.

Library Room menyediakan anotasi kemudahan, seperti @Insert, @Delete, dan @Update, untuk menentukan metode yang menjalankan penyisipan, penghapusan, dan pembaruan sederhana tanpa mengharuskan Anda menulis pernyataan SQL.

Jika Anda perlu menentukan operasi yang lebih kompleks untuk menyisipkan, menghapus, memperbarui, atau jika perlu membuat kueri data dalam database, gunakan anotasi @Query.

Sebagai bonus tambahan, saat Anda menulis kueri di Android Studio, compiler akan memeriksa kueri SQL untuk menemukan error sintaksis.

Untuk aplikasi Inventaris, Anda memerlukan kemampuan untuk melakukan hal berikut:

  • Sisipkan atau tambahkan item baru.
  • Update item yang ada untuk memperbarui nama, harga, dan kuantitas.
  • Dapatkan item tertentu berdasarkan kunci utamanya, id.
  • Dapatkan semua item sehingga Anda dapat menampilkannya.
  • Hapus entri di database.

59aaa051e6a22e79.png

Selesaikan langkah-langkah berikut untuk menerapkan DAO item di aplikasi Anda:

  1. Di paket data, buat antarmuka Kotlin ItemDao.kt.

kolom nama diisi sebagai dao item

  1. Anotasi antarmuka ItemDao dengan @Dao.
import androidx.room.Dao

@Dao
interface ItemDao {
}
  1. Di dalam isi antarmuka, tambahkan anotasi @Insert.
  2. Di bawah @Insert, tambahkan fungsi insert() yang menggunakan instance class Entity item sebagai argumennya.
  3. Tandai fungsi dengan kata kunci suspend agar dapat berjalan di thread terpisah.

Operasi database dapat memerlukan waktu lama untuk dijalankan sehingga harus berjalan di thread terpisah. Room tidak mengizinkan akses database pada thread utama.

import androidx.room.Insert

@Insert
suspend fun insert(item: Item)

Ketika menyisipkan item ke dalam database, konflik dapat terjadi. Misalnya, beberapa tempat dalam kode mencoba memperbarui entity dengan nilai yang berbeda dan bertentangan, seperti kunci utama yang sama. Entity merupakan baris di DB. Di aplikasi Inventory, kita hanya menyisipkan entity dari satu tempat yang merupakan layar Add Item sehingga tidak terjadi konflik dan kita dapat menetapkan strategi konflik ke Ignore.

  1. Tambahkan argumen onConflict dan tetapkan nilai OnConflictStrategy.IGNORE.

Argumen onConflict memberi tahu Room apa yang harus dilakukan jika terjadi konflik. Strategi OnConflictStrategy.IGNORE mengabaikan item baru.

Untuk mengetahui lebih lanjut strategi konflik yang tersedia, lihat dokumentasiOnConflictStrategy.

import androidx.room.OnConflictStrategy

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

Sekarang Room menghasilkan semua kode yang diperlukan untuk memasukkan item ke dalam database. Saat Anda memanggil salah satu fungsi DAO yang ditandai dengan anotasi Room, Room akan mengeksekusi kueri SQL terkait pada database. Misalnya, saat Anda memanggil metode di atas, insert() dari kode Kotlin Anda, Room akan mengeksekusi kueri SQL untuk memasukkan entity ke dalam database.

  1. Tambahkan fungsi baru dengan anotasi @Update yang menggunakan Item sebagai parameter.

Entity yang diperbarui memiliki kunci utama yang sama dengan entity yang diteruskan. Anda dapat memperbarui beberapa atau semua properti lainnya dari entity tersebut.

  1. Serupa dengan metode insert(), tandai fungsi ini dengan kata kunci suspend.
import androidx.room.Update

@Update
suspend fun update(item: Item)

Tambahkan fungsi lain dengan anotasi @Delete untuk menghapus item, dan jadikan sebagai fungsi penangguhan.

import androidx.room.Delete

@Delete
suspend fun delete(item: Item)

Tidak ada anotasi kemudahan untuk fungsi yang tersisa, sehingga Anda harus menggunakan anotasi @Query dan menyediakan kueri SQLite.

  1. Tulis kueri SQLite untuk mengambil item tertentu dari tabel item berdasarkan id yang diberikan. Kode berikut memberikan contoh kueri yang memilih semua kolom dari items, dengan id yang cocok dengan nilai tertentu dan id adalah ID unik.

Contoh:

// Example, no need to copy over
SELECT * from items WHERE id = 1
  1. Tambahkan anotasi @Query.
  2. Gunakan kueri SQLite dari langkah sebelumnya sebagai parameter string ke anotasi @Query.
  3. Tambahkan parameter String ke @Query yang merupakan kueri SQLite untuk mengambil item dari tabel item.

Kueri sekarang meminta untuk memilih semua kolom dari items, dengan id yang cocok dengan argumen id. Perhatikan bahwa :id menggunakan notasi titik dua di kueri untuk mereferensikan argumen dalam fungsi.

@Query("SELECT * from items WHERE id = :id")
  1. Setelah anotasi @Query, tambahkan fungsi getItem() yang menggunakan argumen Int dan menampilkan Flow<Item>.
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Query("SELECT * from items WHERE id = :id")
fun getItem(id: Int): Flow<Item>

Sebaiknya gunakan Flow di layer persistensi. Dengan Flow sebagai jenis nilai yang ditampilkan, Anda akan menerima notifikasi setiap kali data dalam database berubah. Room terus memperbarui Flow ini, yang berarti Anda hanya perlu mendapatkan data secara eksplisit satu kali. Penyiapan ini berguna untuk memperbarui daftar inventaris, yang Anda terapkan di codelab berikutnya. Karena jenis nilai yang ditampilkan Flow, Room juga menjalankan kueri pada thread latar belakang. Anda tidak perlu membuatnya secara eksplisit sebagai fungsi suspend dan memanggilnya di dalam cakupan coroutine.

  1. Tambahkan @Query dengan fungsi getAllItems().
  2. Minta kueri SQLite untuk menampilkan semua kolom dari tabel item yang diurutkan dalam urutan menaik.
  3. Minta getAllItems() menampilkan daftar entity Item sebagai Flow. Room terus memperbarui Flow ini, yang berarti Anda hanya perlu mendapatkan data secara eksplisit satu kali.
@Query("SELECT * from items ORDER BY name ASC")
fun getAllItems(): Flow<List<Item>>

Menyelesaikan ItemDao:

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow

@Dao
interface ItemDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(item: Item)

    @Update
    suspend fun update(item: Item)

    @Delete
    suspend fun delete(item: Item)

    @Query("SELECT * from items WHERE id = :id")
    fun getItem(id: Int): Flow<Item>

    @Query("SELECT * from items ORDER BY name ASC")
    fun getAllItems(): Flow<List<Item>>
}
  1. Meskipun Anda tidak akan melihat perubahan apa pun yang terlihat, bangun aplikasi Anda untuk memastikan tidak ada error.

7. Membuat instance Database

Dalam tugas ini, Anda akan membuat RoomDatabase yang menggunakan Entity dan DAO dari tugas sebelumnya. Class database menentukan daftar entity dan DAO.

Class Database memberikan instance DAO yang Anda tentukan untuk aplikasi. Selanjutnya, aplikasi dapat menggunakan DAO untuk mengambil data dari database sebagai instance dari objek entity data terkait. Aplikasi juga dapat menggunakan entity data yang ditentukan untuk memperbarui baris dari tabel yang sesuai atau membuat baris baru untuk penyisipan.

Anda perlu membuat class RoomDatabase abstrak dan menganotasinya dengan @Database. Class ini memiliki satu metode yang menampilkan instance RoomDatabase yang ada jika database tidak ada.

Berikut adalah proses umum untuk mendapatkan instance RoomDatabase:

  • Buat class public abstract yang memperluas RoomDatabase. Class abstrak baru yang Anda tentukan berfungsi sebagai holder database. Class yang Anda tentukan bersifat abstrak, karena Room yang akan membuatkan implementasi untuk Anda.
  • Anotasikan class dengan @Database. Dalam argumen, cantumkan entity untuk database dan tetapkan nomor versinya.
  • Tentukan metode atau properti abstrak yang menampilkan instance ItemDao, dan Room akan menghasilkan implementasinya untuk Anda.
  • Anda hanya memerlukan satu instance RoomDatabase untuk seluruh aplikasi, sehingga jadikan RoomDatabase sebuah singleton.
  • Gunakan Room.databaseBuilder Room untuk membuat database (item_database) hanya jika tidak ada. Jika tidak, tampilkan database yang ada.

Membuat Database

  1. Di paket data, buat class Kotlin InventoryDatabase.kt.
  2. Di file InventoryDatabase.kt, buat class InventoryDatabase sebagai class abstract yang memperluas RoomDatabase.
  3. Anotasikan class dengan @Database. Abaikan error parameter yang tidak ada, yang akan Anda perbaiki di langkah berikutnya.
import androidx.room.Database
import androidx.room.RoomDatabase

@Database
abstract class InventoryDatabase : RoomDatabase() {}

Anotasi @Database memerlukan beberapa argumen sehingga Room dapat membuat database.

  1. Tentukan Item sebagai satu-satunya class dengan daftar entities.
  2. Setel version sebagai 1. Setiap kali mengubah skema tabel database, Anda harus meningkatkan nomor versinya.
  3. Setel exportSchema ke false agar tidak menyimpan cadangan histori versi skema.
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. Di dalam isi class, deklarasikan fungsi abstrak yang menampilkan ItemDao sehingga database mengetahui DAO.
abstract fun itemDao(): ItemDao
  1. Di bawah fungsi abstrak, tentukan companion object, yang memungkinkan akses ke metode untuk membuat atau mendapatkan database dan menggunakan nama class sebagai penentu.
 companion object {}
  1. Di dalam objek companion, deklarasikan variabel nullable pribadi Instance untuk database lalu inisialisasikan ke null.

Variabel Instance akan menyimpan referensi ke database ketika salah satunya telah dibuat. Hal ini membantu mempertahankan satu instance dari database yang dibuka pada waktu tertentu, yang merupakan resource mahal untuk dibuat dan dikelola.

  1. Anotasikan Instance dengan @Volatile.

Nilai variabel yang tidak stabil tidak pernah disimpan dalam cache, dan semua pembacaan dan penulisan dilakukan ke dan dari memori utama. Fitur ini membantu memastikan nilai Instance selalu yang terbaru dan sama untuk semua thread eksekusi. Hal ini berarti perubahan yang dibuat oleh satu thread ke Instance akan langsung terlihat oleh semua thread lainnya.

@Volatile
private var Instance: InventoryDatabase? = null
  1. Di bawah Instance, saat masih berada di dalam objek companion, tentukan metode getDatabase() dengan parameter Context yang diperlukan builder database.
  2. Tampilkan jenis InventoryDatabase. Pesan error muncul karena getDatabase() belum menampilkan apa pun.
import android.content.Context

fun getDatabase(context: Context): InventoryDatabase {}

Beberapa thread dapat berpotensi meminta instance database secara bersamaan sehingga menghasilkan dua database, bukan satu. Masalah ini dikenal sebagai kondisi race. Dengan menggabungkan kode untuk mendapatkan database di dalam blok synchronized berarti hanya satu thread eksekusi dalam satu waktu yang dapat memasukkan blok kode ini, yang memastikan bahwa database hanya diinisialisasi sekali. Gunakan blok synchronized{} untuk menghindari kondisi race.

  1. Dalam getDatabase(), tampilkan variabel Instance. Atau, jika Instance adalah null, lakukan inisialisasi di dalam blok synchronized{}. Gunakan operator elvis (?:) untuk melakukannya.
  2. Teruskan this, objek pendamping. Anda dapat memperbaiki error tersebut di langkah berikutnya.
return Instance ?: synchronized(this) { }
  1. Di dalam blok yang disinkronkan, gunakan builder database untuk mendapatkan database. Lanjutkan untuk mengabaikan error, yang akan Anda perbaiki di langkah berikutnya.
import androidx.room.Room

Room.databaseBuilder()
  1. Di dalam blok synchronized, gunakan builder database untuk mendapatkan database. Teruskan konteks aplikasi, class database, serta nama untuk database- item_database ke Room.databaseBuilder().
Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")

Android Studio menghasilkan error Ketidakcocokan Jenis. Untuk menghapus error ini, Anda harus menambahkan build() di langkah berikut.

  1. Tambahkan strategi migrasi yang diperlukan ke builder. Gunakan . fallbackToDestructiveMigration().
.fallbackToDestructiveMigration()
  1. Untuk membuat instance database, panggil .build(). Panggilan ini akan menghapus error Android Studio.
.build()
  1. Setelah build(), tambahkan blok also dan tetapkan Instance = it untuk mempertahankan referensi ke instance db yang baru dibuat.
.also { Instance = it }
  1. Di akhir blok synchronized, tampilkan instance. Kode akhir Anda akan terlihat seperti kode berikut:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

/**
* Database class with a singleton Instance object.
*/
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class InventoryDatabase : RoomDatabase() {

    abstract fun itemDao(): ItemDao

    companion object {
        @Volatile
        private var Instance: InventoryDatabase? = null

        fun getDatabase(context: Context): InventoryDatabase {
            // if the Instance is not null, return it, otherwise create a new database instance.
            return Instance ?: synchronized(this) {
                Room.databaseBuilder(context, InventoryDatabase::class.java, "item_database")
                    .build()
                    .also { Instance = it }
            }
        }
    }
}
  1. Buat kode untuk memastikan tidak ada error.

8. Mengimplementasikan Repositori

Dalam tugas ini, Anda akan mengimplementasikan antarmuka ItemsRepository dan class OfflineItemsRepository untuk menyediakan entity get, insert, delete, dan update dari database.

  1. Buka file ItemsRepository.kt pada paket data.
  2. Tambahkan fungsi berikut ke antarmuka, yang memetakan ke implementasi DAO.
import kotlinx.coroutines.flow.Flow

/**
* Repository that provides insert, update, delete, and retrieve of [Item] from a given data source.
*/
interface ItemsRepository {
    /**
     * Retrieve all the items from the the given data source.
     */
    fun getAllItemsStream(): Flow<List<Item>>

    /**
     * Retrieve an item from the given data source that matches with the [id].
     */
    fun getItemStream(id: Int): Flow<Item?>

    /**
     * Insert item in the data source
     */
    suspend fun insertItem(item: Item)

    /**
     * Delete item from the data source
     */
    suspend fun deleteItem(item: Item)

    /**
     * Update item in the data source
     */
    suspend fun updateItem(item: Item)
}
  1. Buka file OfflineItemsRepository.kt pada paket data.
  2. Teruskan parameter konstruktor jenis ItemDao.
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
  1. Di class OfflineItemsRepository, ganti fungsi yang ditentukan di antarmuka ItemsRepository dan panggil fungsi yang sesuai dari ItemDao.
import kotlinx.coroutines.flow.Flow

class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository {
    override fun getAllItemsStream(): Flow<List<Item>> = itemDao.getAllItems()

    override fun getItemStream(id: Int): Flow<Item?> = itemDao.getItem(id)

    override suspend fun insertItem(item: Item) = itemDao.insert(item)

    override suspend fun deleteItem(item: Item) = itemDao.delete(item)

    override suspend fun updateItem(item: Item) = itemDao.update(item)
}

Mengimplementasikan class AppContainer

Di tugas ini, Anda akan membuat instance database dan meneruskan instance DAO ke class OfflineItemsRepository.

  1. Buka file AppContainer.kt pada paket data.
  2. Teruskan instance ItemDao() ke konstruktor OfflineItemsRepository.
  3. Buat instance database dengan memanggil getDatabase() pada class InventoryDatabase yang meneruskan konteks, lalu memanggil .itemDao() untuk membuat instance Dao.
override val itemsRepository: ItemsRepository by lazy {
    OfflineItemsRepository(InventoryDatabase.getDatabase(context).itemDao())
}

Sekarang Anda memiliki semua elemen penyusun untuk menggunakan Room. Kode ini dikompilasi dan dijalankan, tetapi Anda tidak dapat mengetahui apakah kode tersebut benar-benar berfungsi. Jadi, ini adalah saat yang tepat untuk menguji database Anda. Untuk menyelesaikan pengujian, Anda memerlukan ViewModel agar dapat berkomunikasi dengan database.

9. Menambahkan fungsi simpan

Sejauh ini Anda telah membuat database, dan class UI merupakan bagian dari kode awal. Untuk menyimpan data sementara aplikasi dan juga mengakses database, Anda perlu mengupdate ViewModel. ViewModel berinteraksi dengan database melalui DAO dan memberikan data ke UI. Semua operasi database harus dijalankan dari UI thread utama. Anda akan melakukannya dengan coroutine dan viewModelScope.

Panduan class status UI

Buka file ui/item/ItemEntryViewModel.kt. Class data ItemUiState mewakili status UI Item. Class data ItemDetails mewakili satu item.

Kode awal memberi Anda tiga fungsi ekstensi:

  • Fungsi ekstensi ItemDetails.toItem() mengonversi objek status UI ItemUiState menjadi jenis entity Item.
  • Fungsi ekstensi Item.toItemUiState() mengonversi objek entity Room Item menjadi jenis status UI ItemUiState.
  • Fungsi ekstensi Item.toItemDetails() mengonversi objek entity Room Item menjadi ItemDetails.
// No need to copy, this is part of starter code
/**
* Represents Ui State for an Item.
*/
data class ItemUiState(
    val itemDetails: ItemDetails = ItemDetails(),
    val isEntryValid: Boolean = false
)

data class ItemDetails(
    val id: Int = 0,
    val name: String = "",
    val price: String = "",
    val quantity: String = "",
)

/**
* Extension function to convert [ItemDetails] to [Item]. If the value of [ItemDetails.price] is
* not a valid [Double], then the price will be set to 0.0. Similarly if the value of
* [ItemDetails.quantity] is not a valid [Int], then the quantity will be set to 0
*/
fun ItemDetails.toItem(): Item = Item(
    id = id,
    name = name,
    price = price.toDoubleOrNull() ?: 0.0,
    quantity = quantity.toIntOrNull() ?: 0
)

fun Item.formatedPrice(): String {
    return NumberFormat.getCurrencyInstance().format(price)
}

/**
* Extension function to convert [Item] to [ItemUiState]
*/
fun Item.toItemUiState(isEntryValid: Boolean = false): ItemUiState = ItemUiState(
    itemDetails = this.toItemDetails(),
    isEntryValid = isEntryValid
)

/**
* Extension function to convert [Item] to [ItemDetails]
*/
fun Item.toItemDetails(): ItemDetails = ItemDetails(
    id = id,
    name = name,
    price = price.toString(),
    quantity = quantity.toString()
)

Anda akan menggunakan class di atas dalam model tampilan untuk membaca dan mengupdate UI.

Mengupdate ViewModel ItemEntry

Dalam tugas ini, Anda meneruskan repositori ke file ItemEntryViewModel.kt. Anda juga menyimpan detail item yang dimasukkan di layar Add Item ke dalam database.

  1. Perhatikan fungsi pribadi validateInput() di class ItemEntryViewModel.
// No need to copy over, this is part of starter code
private fun validateInput(uiState: ItemDetails = itemUiState.itemDetails): Boolean {
    return with(uiState) {
        name.isNotBlank() && price.isNotBlank() && quantity.isNotBlank()
    }
}

Fungsi di atas memeriksa apakah name, price, dan quantity kosong. Anda akan menggunakan fungsi ini untuk memverifikasi input pengguna sebelum menambah atau memperbarui entity dalam database.

  1. Buka class ItemEntryViewModel dan tambahkan parameter konstruktor default private dari jenis ItemsRepository.
import com.example.inventory.data.ItemsRepository

class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
  1. Update initializer untuk model tampilan entri item di ui/AppViewModelProvider.kt dan teruskan instance repositori sebagai parameter.
object AppViewModelProvider {
    val Factory = viewModelFactory {
        // Other Initializers
        // Initializer for ItemEntryViewModel
        initializer {
            ItemEntryViewModel(inventoryApplication().container.itemsRepository)
        }
        //...
    }
}
  1. Buka file ItemEntryViewModel.kt dan di akhir class ItemEntryViewModel, lalu tambahkan fungsi penangguhan bernama saveItem() untuk menyisipkan item ke dalam database Room. Fungsi ini menambahkan data ke database dengan cara yang tidak memblokir.
suspend fun saveItem() {
}
  1. Di dalam fungsi, periksa apakah itemUiState valid, lalu konversikan ke jenis Item agar Room dapat memahami data.
  2. Panggil insertItem() pada itemsRepository dan teruskan data. UI memanggil fungsi ini untuk menambahkan detail Item ke database.
suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}

Anda telah menambahkan semua fungsi yang diperlukan untuk menambahkan entity ke database. Pada tugas berikutnya, Anda akan mengupdate UI untuk menggunakan fungsi di atas.

Panduan composable ItemEntryBody()

  1. Dalam file ui/item/ItemEntryScreen.kt, composable ItemEntryBody() diterapkan sebagian untuk Anda sebagai bagian dari kode awal. Lihat composable ItemEntryBody() dalam panggilan fungsi ItemEntryScreen().
// No need to copy over, part of the starter code
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = Modifier
        .padding(innerPadding)
        .verticalScroll(rememberScrollState())
        .fillMaxWidth()
)
  1. Perhatikan bahwa status UI dan lambda updateUiState diteruskan sebagai parameter fungsi. Lihat definisi fungsi untuk mengetahui cara status UI diupdate.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    itemUiState: ItemUiState,
    onItemValueChange: (ItemUiState) -> Unit,
    onSaveClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             onValueChange = onItemValueChange,
             modifier = Modifier.fillMaxWidth()
         )
        Button(
             onClick = onSaveClick,
             enabled = itemUiState.isEntryValid,
             shape = MaterialTheme.shapes.small,
             modifier = Modifier.fillMaxWidth()
         ) {
             Text(text = stringResource(R.string.save_action))
         }
    }
}

Anda menampilkan ItemInputForm dan tombol Save dalam composable ini. Dalam composable ItemInputForm(), Anda menampilkan tiga kolom teks. Opsi Save hanya diaktifkan jika teks dimasukkan di kolom teks. Nilai isEntryValid bernilai benar jika teks di semua kolom teks valid (tidak kosong).

Layar ponsel dengan detail item yang terisi sebagian dan tombol simpan dinonaktifkan

Layar ponsel dengan detail item yang terisi dan tombol simpan diaktifkan

  1. Lihat implementasi fungsi composable ItemInputForm() dan perhatikan parameter fungsi onValueChange. Anda memperbarui nilai itemDetails dengan nilai yang dimasukkan oleh pengguna di kolom teks. Saat tombol Save diaktifkan, itemUiState.itemDetails memiliki nilai yang perlu disimpan.
// No need to copy over, part of the starter code
@Composable
fun ItemEntryBody(
    //...
) {
    Column(
        // ...
    ) {
        ItemInputForm(
             itemDetails = itemUiState.itemDetails,
             //...
         )
        //...
    }
}
// No need to copy over, part of the starter code
@Composable
fun ItemInputForm(
    itemDetails: ItemDetails,
    modifier: Modifier = Modifier,
    onValueChange: (ItemUiState) -> Unit = {},
    enabled: Boolean = true
) {
    Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        OutlinedTextField(
            value = itemUiState.name,
            onValueChange = { onValueChange(itemDetails.copy(name = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.price,
            onValueChange = { onValueChange(itemDetails.copy(price = it)) },
            //...
        )
        OutlinedTextField(
            value = itemUiState.quantity,
            onValueChange = { onValueChange(itemDetails.copy(quantity = it)) },
            //...
        )
    }
}

Menambahkan pemroses klik ke tombol Save

Untuk menggabungkan semuanya, tambahkan pengendali klik ke tombol Save. Dalam pengendali klik, Anda meluncurkan coroutine dan memanggil saveItem() untuk menyimpan data di database Room.

  1. Di ItemEntryScreen.kt, dalam fungsi composable ItemEntryScreen, buat val bernama coroutineScope dengan fungsi composable rememberCoroutineScope().
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Update panggilan fungsi ItemEntryBody() dan luncurkan coroutine dalam lambda onSaveClick.
ItemEntryBody(
   // ...
    onSaveClick = {
        coroutineScope.launch {
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Lihat implementasi fungsi saveItem() di file ItemEntryViewModel.kt untuk memeriksa apakah itemUiState valid, mengonversi itemUiState menjadi jenis Item, dan menyisipkannya dalam database menggunakan itemsRepository.insertItem().
// No need to copy over, you have already implemented this as part of the Room implementation

suspend fun saveItem() {
    if (validateInput()) {
        itemsRepository.insertItem(itemUiState.itemDetails.toItem())
    }
}
  1. Di ItemEntryScreen.kt, dalam fungsi composable ItemEntryScreen, di dalam coroutine, panggil viewModel.saveItem() untuk menyimpan item dalam database.
ItemEntryBody(
    // ...
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
        }
    },
    //...
)

Perhatikan bahwa Anda tidak menggunakan viewModelScope.launch() untuk saveItem() dalam file ItemEntryViewModel.kt, tetapi hal ini diperlukan untuk ItemEntryBody() saat Anda memanggil metode repositori. Anda hanya dapat memanggil fungsi penangguhan dari coroutine atau fungsi penangguhan lainnya. Fungsi viewModel.saveItem() merupakan fungsi penangguhan.

  1. Bangun dan jalankan aplikasi Anda.
  2. Ketuk + FAB.
  3. Di layar Add Item, tambahkan detail item dan ketuk Save. Perhatikan bahwa mengetuk tombol Save tidak akan menutup layar Add Item.

Layar ponsel dengan detail item yang terisi dan tombol simpan diaktifkan

  1. Di lambda onSaveClick, tambahkan panggilan ke navigateBack() setelah panggilan ke viewModel.saveItem() untuk membuka kembali layar sebelumnya. Fungsi ItemEntryBody() terlihat seperti kode berikut:
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = {
        coroutineScope.launch {
            viewModel.saveItem()
            navigateBack()
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Jalankan kembali aplikasi dan lakukan langkah yang sama untuk memasukkan dan menyimpan data. Perhatikan bahwa kali ini aplikasi kembali ke layar Inventory.

Tindakan ini akan menyimpan data, tetapi Anda tidak dapat melihat data inventaris di aplikasi. Pada tugas berikutnya, Anda dapat menggunakan Database Inspector untuk melihat data yang disimpan.

Layar aplikasi dengan daftar inventaris kosong

10. Melihat isi database menggunakan Database Inspector

Database Inspector memungkinkan Anda memeriksa, membuat kueri, dan mengubah database aplikasi saat aplikasi sedang berjalan. Fitur ini sangat berguna untuk proses debug database. Database Inspector bekerja dengan SQLite biasa dan library yang dibuat pada SQLite, seperti Room. Database Inspector berfungsi paling baik pada emulator/perangkat yang menjalankan API level 26.

  1. Jalankan aplikasi Anda di emulator atau perangkat terhubung yang menjalankan API level 26 atau lebih tinggi, jika Anda belum melakukannya.
  2. Di Android Studio, pilih View > Tool Windows > App Inspection dari panel menu.
  3. Pilih tab Database Inspector.
  4. Di panel Database Inspector, pilih com.example.inventory dari menu dropdown jika belum dipilih. item_database di aplikasi Inventory akan muncul di panel Databases.

76408bd5e93c3432.png

  1. Luaskan node untuk item_database di panel Databases dan pilih Item untuk diperiksa. Jika panel Databases Anda kosong, gunakan emulator untuk menambahkan beberapa item ke database menggunakan layar Add Item.
  2. Centang kotak Live updates di Database Inspector untuk otomatis memperbarui data yang ditampilkan saat Anda berinteraksi dengan aplikasi yang berjalan di emulator atau perangkat.

9e21d9f7eb426008.png

Selamat! Anda telah membuat aplikasi yang dapat mempertahankan data menggunakan Room. Pada codelab berikutnya, Anda akan menambahkan lazyColumn ke aplikasi untuk menampilkan item pada database, serta menambahkan fitur baru ke aplikasi, seperti kemampuan untuk menghapus dan memperbarui entity. Sampai jumpa!

11. Mendapatkan kode solusi

Kode solusi untuk codelab ini ada di repo GitHub. Untuk mendownload kode codelab yang sudah selesai, gunakan perintah git berikut:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-inventory-app.git
$ cd basic-android-kotlin-compose-training-inventory-app
$ git checkout room

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

Jika Anda ingin melihat kode solusi untuk codelab ini, lihat kode tersebut di GitHub.

12. Ringkasan

  • Tentukan tabel Anda sebagai class data yang dianotasi dengan @Entity. Tentukan properti yang dianotasi dengan @ColumnInfo sebagai kolom dalam tabel.
  • Tentukan objek akses data (DAO) sebagai antarmuka yang dianotasi dengan @Dao. DAO memetakan fungsi Kotlin ke kueri database.
  • Gunakan anotasi untuk menentukan fungsi @Insert, @Delete, dan @Update.
  • Gunakan anotasi @Query dengan string kueri SQLite sebagai parameter untuk kueri lainnya.
  • Gunakan Database Inspector untuk melihat data yang disimpan di database Android SQLite.

13. Pelajari lebih lanjut

Dokumentasi Developer Android

Postingan blog

Video

Dokumentasi dan artikel lainnya