Mempertahankan data dengan Room

1. Sebelum memulai

Sebagian besar aplikasi berkualitas produksi memiliki data yang perlu disimpan, bahkan setelah pengguna menutup aplikasi. Misalnya, aplikasi dapat menyimpan playlist lagu, item dalam daftar tugas, catatan pengeluaran dan pendapatan, katalog konstelasi, atau histori data pribadi. Untuk sebagian besar kasus ini, Anda menggunakan database untuk 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. Room lebih menyederhanakan tugas-tugas penyiapan, konfigurasi, dan interaksi dengan database daripada menggunakan SQLite secara langsung. Room juga menyediakan pemeriksaan waktu kompilasi terhadap pernyataan SQLite.

Gambar di bawah menunjukkan kesesuaian Room dengan arsitektur keseluruhan yang direkomendasikan dalam kursus ini.

7521165e051cc0d4.png

Prasyarat

  • Mengetahui cara mem-build antarmuka pengguna (UI) dasar untuk aplikasi Android.
  • Mengetahui cara menggunakan aktivitas, fragmen, dan tampilan.
  • Mengetahui cara beralih di antara fragmen, dengan menggunakan Safe Args untuk meneruskan data di antara fragmen.
  • Memahami komponen arsitektur Android ViewModel, LiveData, dan Flow, serta mengetahui cara menggunakan ViewModelProvider.Factory untuk membuat instance ViewModels.
  • Memahami dasar-dasar konkurensi.
  • Mengetahui cara menggunakan coroutine untuk tugas yang berjalan lama.
  • Memiliki pemahaman dasar tentang database SQL dan bahasa SQLite.

Yang akan Anda pelajari

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

Yang akan Anda build

  • Anda akan mem-build aplikasi Inventory yang menyimpan item inventaris ke dalam database SQLite.

Yang Anda perlukan

  • Kode awal untuk aplikasi Inventory.
  • Komputer yang dilengkapi Android Studio.

2. Ringkasan aplikasi

Dalam codelab ini, Anda akan menggunakan aplikasi awal yang disebut aplikasi Inventory, dan menambahkan lapisan database ke dalamnya menggunakan library Room. Versi final aplikasi menampilkan item daftar dari database inventory menggunakan RecyclerView. Pengguna akan memiliki opsi untuk menambahkan item baru, memperbarui item yang sudah ada, dan menghapus item dari database inventory (Anda akan menyelesaikan fungsionalitas aplikasi di codelab berikutnya).

Berikut adalah screenshot dari versi final aplikasi.

439ad9a8183278c5.png

3. Ringkasan aplikasi awal

Mendownload kode awal untuk codelab ini

Codelab ini menyediakan kode awal bagi Anda untuk diperluas dengan fitur yang dipelajari dalam codelab ini. Kode awal dapat berisi kode yang tidak asing bagi Anda dari codelab sebelumnya dan juga kode yang tidak Anda ketahui yang akan Anda pelajari di codelab berikutnya.

Jika Anda menggunakan kode awal dari GitHub, pastikan nama foldernya adalah android-basics-kotlin-inventory-app-starter. Pilih folder ini saat Anda membuka project di Android Studio.

Untuk mendapatkan kode codelab ini dan membukanya di Android Studio, lakukan hal berikut.

Mendapatkan kode

  1. Klik URL yang diberikan. Tindakan ini akan membuka halaman GitHub project di browser.
  2. Di halaman GitHub project, klik tombol Code yang akan menampilkan dialog.

5b0a76c50478a73f.png

  1. Di dialog, klik tombol Download ZIP untuk menyimpan project di komputer. Tunggu download selesai.
  2. Temukan file di komputer Anda (mungkin di folder Downloads).
  3. Klik dua kali pada file ZIP untuk mengekstraknya. Tindakan ini akan membuat folder baru yang berisi file project.

Membuka project di Android Studio

  1. Mulai Android Studio.
  2. Di jendela Welcome to Android Studio, klik Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > New > Import Project.

21f3eec988dcfbe9.png

  1. Di dialog Import Project, buka lokasi folder project yang telah diekstrak (kemungkinan ada di folder Downloads).
  2. Klik dua kali pada folder project tersebut.
  3. Tunggu Android Studio membuka project.
  4. Klik tombol Run 11c34fc5e516fb1c.png untuk mem-build dan menjalankan aplikasi. Pastikan aplikasi di-build seperti yang diharapkan.
  5. Cari file project di jendela alat Project untuk melihat cara aplikasi disiapkan.

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 menjalankan API level 26 atau lebih tinggi. Database Inspector berfungsi paling baik pada emulator/perangkat yang menjalankan API level 26.
  3. Aplikasi tidak menampilkan data inventaris. Perhatikan FAB untuk menambahkan item baru ke database.
  4. Klik FAB. Aplikasi akan membuka layar baru tempat Anda dapat memasukkan detail item baru.

9c5e361a89453821.png

Masalah dengan kode awal

  1. Di layar Add Item, masukkan detail item. Ketuk Save. Fragmen add item tidak ditutup. Kembali menggunakan tombol kembali sistem. Item baru tidak disimpan dan tidak tercantum di layar inventory. Perhatikan bahwa aplikasi tidak lengkap dan fungsi tombol Save tidak diterapkan.

f0931dab5089a14f.png

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

Panduan kode

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

main_activity.xml

Aktivitas utama yang menghosting semua fragmen lain dalam aplikasi. Metode onCreate() mengambil NavController dari NavHostFragment dan menyiapkan panel tindakan untuk digunakan dengan NavController.

item_list_fragment.xml

Layar pertama yang ditampilkan di aplikasi. Layar ini terutama berisi RecyclerView dan FAB. Anda akan mengimplementasikan RecyclerView nanti di jalur tersebut.

fragment_add_item.xml

Tata letak ini berisi kolom teks untuk memasukkan detail item inventaris baru yang akan ditambahkan.

ItemListFragment.kt

Fragmen ini sebagian besar berisi kode boilerplate. Pada metode onViewCreated(), pemroses klik disetel pada FAB untuk membuka fragmen add item.

AddItemFragment.kt

Fragmen ini digunakan untuk menambahkan item baru ke dalam database. Fungsi onCreateView() menginisialisasi variabel binding dan fungsi onDestroyView() menyembunyikan keyboard sebelum menghancurkan fragmen.

4. Komponen utama Room

Kotlin menyediakan cara mudah untuk menangani data dengan memperkenalkan class data. Data ini diakses dan mungkin dimodifikasi menggunakan panggilan fungsi. Namun, dalam dunia database, Anda memerlukan tabel dan kueri untuk mengakses dan mengubah data. Komponen Room berikut membuat alur kerja ini menjadi lancar.

Terdapat tiga komponen utama dalam Room:

  • Entity data menampilkan tabel di database aplikasi Anda. Entity data digunakan untuk memperbarui data yang disimpan dalam baris di tabel, dan untuk membuat baris baru untuk penyisipan.
  • Objek akses data (DAO) menyediakan metode yang digunakan oleh aplikasi Anda untuk mengambil, memperbarui, menyisipkan, dan menghapus data dalam database.
  • Class database memiliki database dan merupakan titik akses utama untuk koneksi yang mendasarinya ke database aplikasi Anda. Class database menyediakan aplikasi Anda dengan instance DAO yang terkait dengan database tersebut.

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.

33a193a68c9a8e0e.png

Menambahkan library Room

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

  1. Buka file gradle level modul, build.gradle (Module: InventoryApp.app). Dalam blok dependencies, tambahkan dependensi berikut untuk library Room.
    // Room
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"

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 akan menyimpan informasi tentang item inventaris seperti nama item, harga item, dan stok yang tersedia.

8c9f1659ee82ca43.png

Anotasi @Entity menandai class sebagai class Entity database. Untuk setiap class Entity, tabel database dibuat untuk menampung 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 ditetapkan, kunci utama tidak dapat diubah, maka akan mewakili objek entity selama ada dalam database.

Dalam tugas ini, Anda akan membuat class Entity. Tentukan kolom untuk menyimpan informasi inventaris berikut untuk setiap item.

  • Int untuk menyimpan kunci utama.
  • String untuk menyimpan nama item.
  • double untuk menyimpan harga item.
  • Int untuk menyimpan jumlah stok.
  1. Buka kode awal di Android Studio.
  2. Buat paket bernama data pada paket dasar com.example.inventory.

be39b42484ba2664.png

  1. Di dalam paket data, buat class Kotlin baru bernama Item. Class ini akan menampilkan entity database di aplikasi Anda. Di langkah berikutnya, Anda akan menambahkan kolom yang sesuai untuk menyimpan informasi inventaris.
  2. Perbarui definisi class Item dengan kode berikut. Deklarasikan id dari jenis Int, itemName dari jenis String, itemPrice dari jenis Double, dan quantityInStock dari jenis Int sebagai parameter untuk konstruktor utama. Tentukan nilai default sebesar 0 ke id. Langkah ini akan menjadi kunci utama, ID untuk mengidentifikasi setiap catatan/entri dalam tabel Item Anda secara unik.
class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)

Class data

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

Contoh:

// Example data class with 2 properties.
data class User(val first_name: String, val last_name: 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 ditandai sebagai val atau var.
  • Class data tidak boleh berupa abstract, open, sealed, atau inner.

Untuk mempelajari class Data lebih lanjut, lihat dokumentasinya.

  1. Konversikan class Item ke class data dengan mengawali definisi class-nya dengan kata kunci data.
data class Item(
   val id: Int = 0,
   val itemName: String,
   val itemPrice: Double,
   val quantityInStock: Int
)
  1. Di atas deklarasi class Item, beri anotasi pada class data dengan @Entity. Gunakan argumen tableName untuk memberikan item sebagai nama tabel SQLite.
@Entity(tableName = "item")
data class Item(
   ...
)
  1. Untuk mengidentifikasi id sebagai kunci utama, beri anotasi pada properti id dengan @PrimaryKey. Setel parameter autoGenerate ke true sehingga Room menghasilkan ID untuk setiap entity. Hal ini menjamin bahwa ID untuk setiap item bersifat unik.
@Entity(tableName = "item")
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   ...
)
  1. Anotasikan properti yang tersisa dengan @ColumnInfo. Anotasi ColumnInfo digunakan untuk menyesuaikan kolom yang terkait dengan bidang tertentu. Misalnya, saat menggunakan argumen name, Anda dapat menentukan nama kolom yang berbeda untuk kolom daripada nama variabel. Sesuaikan nama properti menggunakan parameter seperti yang ditunjukkan di bawah ini. Pendekatan ini mirip dengan menggunakan tableName untuk menentukan nama yang berbeda dengan database.
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

6. Membuat DAO item

Objek Akses Data (DAO)

Objek Akses Data (DAO) adalah pola yang digunakan untuk memisahkan lapisan persistensi dengan 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 terlibat dalam menjalankan operasi database dalam lapisan persistensi yang mendasari dari bagian aplikasi lainnya. Hal ini memungkinkan lapisan akses data diubah secara terpisah dari kode yang menggunakan data.

7a8480711f04b3ef.png

Dalam tugas ini, Anda akan menentukan Objek Akses Data (DAO) untuk Room. Objek akses data merupakan komponen utama Room yang bertanggung jawab untuk menentukan antarmuka yang mengakses database.

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

Untuk operasi database umum, library Room menyediakan anotasi kemudahan, seperti @Insert, @Delete, dan @Update. Untuk lainnya, terdapat anotasi @Query. Anda dapat menulis kueri apa pun yang didukung oleh SQLite.

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

Untuk aplikasi inventory, Anda harus dapat melakukan hal berikut:

  • Sisipkan atau tambahkan item baru.
  • Perbarui 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.

bb381857d5fba511.png

Sekarang, implementasikan DAO item di aplikasi Anda:

  1. Dalam paket data, buat class Kotlin ItemDao.kt.
  2. Ubah definisi class menjadi interface dan beri anotasi dengan @Dao.
@Dao
interface ItemDao {
}
  1. Di dalam isi antarmuka, tambahkan anotasi @Insert. Di bawah @Insert, tambahkan fungsi insert() yang menggunakan instance class Entity item sebagai argumennya. Operasi database dapat memerlukan waktu yang lama untuk dijalankan, sehingga harus berjalan di thread terpisah. Buat fungsi sebagai fungsi penangguhan, sehingga fungsi ini dapat dipanggil dari coroutine.
@Insert
suspend fun insert(item: Item)
  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 jika kunci utama tersebut sudah ada dalam database. Untuk mengetahui lebih lanjut strategi konflik yang tersedia, lihat dokumentasi.
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)

Sekarang Room akan membuat semua kode yang diperlukan untuk memasukkan item ke dalam database. Saat Anda memanggil insert() dari kode Kotlin, Room akan menjalankan kueri SQL untuk memasukkan entity ke dalam database. (Catatan: Fungsi ini dapat diberi nama apa pun yang Anda inginkan, tetapi tidak harus disebut insert().)

  1. Tambahkan anotasi @Update dengan fungsi update() untuk satu item. Entity yang diperbarui memiliki kunci yang sama dengan entity yang diteruskan. Anda dapat memperbarui beberapa atau semua properti lainnya dari entity tersebut. Serupa dengan metode insert(), jadikan metode update() berikut sebagai suspend.
@Update
suspend fun update(item: Item)
  1. Tambahkan anotasi @Delete dengan fungsi delete() untuk menghapus item. Jadikan sebagai metode penangguhan. Anotasi @Delete menghapus satu item, atau sebuah daftar item. (Catatan: Anda harus meneruskan entity untuk dihapus. Jika tidak memiliki entity, Anda mungkin harus mengambilnya sebelum memanggil fungsi 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. Anda kemudian akan menambahkan anotasi Room dan menggunakan versi kueri berikut yang dimodifikasi pada langkah berikutnya. Di langkah berikutnya, Anda juga akan mengubahnya menjadi metode DAO menggunakan Room.
  2. Pilih semua kolom dari item
  3. WHERE id cocok dengan nilai tertentu.

Contoh:

SELECT * from item WHERE id = 1
  1. Ubah kueri SQL di atas untuk digunakan oleh anotasi Room dan argumen. Tambahkan anotasi @Query, sediakan kueri sebagai parameter string ke anotasi @Query. Tambahkan parameter String ke @Query yang merupakan kueri SQLite untuk mengambil item dari tabel item.
  2. Pilih semua kolom dari item
  3. WHERE id cocok dengan argumen id. Perhatikan :id. Anda dapat menggunakan notasi titik dua di kueri untuk mereferensikan argumen dalam fungsi.
@Query("SELECT * from item WHERE id = :id")
  1. Di bawah anotasi @Query, tambahkan fungsi getItem() yang menggunakan argumen Int serta tampilkan Flow<Item>.
@Query("SELECT * from item WHERE id = :id")
fun getItem(id: Int): Flow<Item>

Menggunakan Flow atau LiveData sebagai jenis nilai yang ditampilkan akan memastikan Anda mendapatkan notifikasi setiap kali data dalam database berubah. Sebaiknya gunakan Flow di lapisan persistensi. Room terus memperbarui Flow ini, yang berarti Anda hanya perlu mendapatkan data secara eksplisit satu kali. Hal ini berguna untuk memperbarui daftar inventaris, yang akan 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 memanggil di dalam cakupan coroutine.

Anda mungkin perlu mengimpor Flow dari kotlinx.coroutines.flow.Flow.

  1. Tambahkan @Query dengan fungsi getItems():
  2. Minta kueri SQLite untuk menampilkan semua kolom dari tabel item yang diurutkan dalam urutan menaik.
  3. Minta getItems() 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 item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
  1. Meskipun Anda tidak akan melihat perubahan apa pun yang terlihat, jalankan aplikasi untuk memastikan tidak ada error.

7. Membuat instance Database

Di tugas ini, Anda akan membuat RoomDatabase dengan menggunakan Entity dan DAO yang telah dibuat di tugas sebelumnya. Class database menentukan daftar entity dan objek akses data. Class database ini juga merupakan titik akses utama untuk koneksi dasar.

Class Database memberikan instance DAO yang telah 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, yang dianotasi dengan @Database. Class ini memiliki satu metode yang dapat membuat instance RoomDatabase yang tidak ada, atau menampilkan instance RoomDatabase yang sudah 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 lalu Room akan menghasilkan penerapan 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 ItemRoomDatabase.kt.
  2. Di file ItemRoomDatabase.kt, buat class ItemRoomDatabase sebagai class abstract yang memperluas RoomDatabase. Anotasikan class dengan @Database. Anda akan memperbaiki error parameter yang hilang pada langkah berikutnya.
@Database
abstract class ItemRoomDatabase : RoomDatabase() {}
  1. Anotasi @Database memerlukan beberapa argumen, sehingga Room dapat membuat database.
  • Tentukan Item sebagai satu-satunya class dengan daftar entities.
  • Setel version sebagai 1. Setiap kali mengubah skema tabel database, Anda harus meningkatkan nomor versinya.
  • Setel exportSchema ke false agar tidak menyimpan cadangan histori versi skema.
@Database(entities = [Item::class], version = 1, exportSchema = false)
  1. Database harus mengetahui DAO. Di dalam isi class, deklarasikan fungsi abstrak yang menampilkan ItemDao. Anda dapat memiliki beberapa DAO.
abstract fun itemDao(): ItemDao
  1. Di bawah fungsi abstrak, tentukan objek companion. Objek pendamping memungkinkan akses ke metode untuk membuat atau mendapatkan database dengan 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.

Anotasikan INSTANCE dengan @Volatile. Nilai variabel yang tidak stabil tidak akan disimpan dalam cache, dan semua penulisan dan pembacaan akan dilakukan ke dan dari memori utama. Hal tersebut akan membantu memastikan agar nilai INSTANCE selalu terbaru dan sama untuk semua thread eksekusi. Ini berarti perubahan yang dibuat oleh satu thread ke INSTANCE akan langsung terlihat oleh semua thread lainnya.

@Volatile
private var INSTANCE: ItemRoomDatabase? = null
  1. Di bawah INSTANCE, saat masih berada di dalam objek companion, tentukan metode getDatabase() dengan parameter Context yang diperlukan builder database. Tampilkan jenis ItemRoomDatabase. Anda akan melihat error karena getDatabase() belum menampilkan apa pun.
fun getDatabase(context: Context): ItemRoomDatabase {}
  1. Beberapa thread dapat berpotensi mengalami kondisi race dan meminta instance database secara bersamaan, sehingga menghasilkan dua database, bukan satu. 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.

Dalam getDatabase(), tampilkan variabel INSTANCE. Atau, jika INSTANCE adalah null, lakukan inisialisasi di dalam blok synchronized{}. Gunakan operator elvis (?:) untuk melakukannya. Teruskan this objek pendamping yang ingin dikunci di dalam blok fungsi. Anda akan memperbaiki error pada langkah berikutnya.

return INSTANCE ?: synchronized(this) { }
  1. Di dalam blok yang disinkronkan, buat variabel instance val, dan gunakan builder database untuk mendapatkan database. Anda masih akan mengalami error yang akan diperbaiki pada langkah berikutnya.
val instance = Room.databaseBuilder()
  1. Di akhir blok synchronized, tampilkan instance.
return instance
  1. Di dalam blok synchronized, lakukan inisialisasi variabel instance, dan gunakan builder database untuk mendapatkan database. Teruskan konteks aplikasi, class database, serta nama untuk database, item_database ke Room.databaseBuilder().
val instance = Room.databaseBuilder(
   context.applicationContext,
   ItemRoomDatabase::class.java,
   "item_database"
)

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

  1. Tambahkan strategi migrasi yang diperlukan ke builder. Gunakan .fallbackToDestructiveMigration().

Biasanya, Anda harus memberikan objek migrasi dengan strategi migrasi saat skema berubah. Objek migrasi adalah objek yang menentukan cara Anda mengambil semua baris dengan skema lama dan mengonversinya menjadi baris dalam skema yang baru, sehingga tidak ada data yang hilang. Migrasi berada di luar cakupan codelab ini. Solusi yang mudah adalah dengan menghancurkan dan membuat ulang database yang berarti data tersebut akan hilang.

.fallbackToDestructiveMigration()
  1. Untuk membuat instance database, panggil .build(). Tindakan ini akan menghapus error Android Studio.
.build()
  1. Di dalam blok synchronized, tetapkan INSTANCE = instance.
INSTANCE = instance
  1. Di akhir blok synchronized, tampilkan instance. Kode final akan terlihat seperti ini:
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase : RoomDatabase() {

   abstract fun itemDao(): ItemDao

   companion object {
       @Volatile
       private var INSTANCE: ItemRoomDatabase? = null
       fun getDatabase(context: Context): ItemRoomDatabase {
           return INSTANCE ?: synchronized(this) {
               val instance = Room.databaseBuilder(
                   context.applicationContext,
                   ItemRoomDatabase::class.java,
                   "item_database"
               )
                   .fallbackToDestructiveMigration()
                   .build()
               INSTANCE = instance
               return instance
           }
       }
   }
}
  1. Buat kode untuk memastikan tidak ada error.

Mengimplementasikan class Application

Di tugas ini, Anda akan membuat instance database di class Application.

  1. Buka InventoryApplication.kt, buat val bernama database dari jenis ItemRoomDatabase. Buat instance database dengan memanggil getDatabase() saat ItemRoomDatabase meneruskan dalam konteks. Gunakan delegasi lazy sehingga instance database akan lambat dibuat saat Anda pertama kali memerlukan/mengakses referensi (bukan saat aplikasi dimulai). Tindakan ini akan membuat database (database fisik pada disk) pada akses pertama.
import android.app.Application
import com.example.inventory.data.ItemRoomDatabase

class InventoryApplication : Application(){
   val database: ItemRoomDatabase by lazy { ItemRoomDatabase.getDatabase(this) }
}

Anda akan menggunakan instance database ini nanti di codelab saat membuat instance ViewModel.

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 waktu yang tepat untuk menambahkan item baru ke database Inventory untuk menguji database Anda. Untuk melakukannya, Anda memerlukan ViewModel agar dapat berkomunikasi dengan database.

8. Menambahkan ViewModel

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

91298a7c05e4f5e0.png

Membuat ViewModel Inventory

  1. Di paket com.example.inventory, buat file class Kotlin InventoryViewModel.kt.
  2. Perluas class InventoryViewModel dari class ViewModel. Teruskan objek ItemDao sebagai parameter ke konstruktor default.
class InventoryViewModel(private val itemDao: ItemDao) : ViewModel() {}
  1. Di akhir file InventoryViewModel.kt di luar class, tambahkan class InventoryViewModelFactory untuk membuat instance InventoryViewModel. Teruskan parameter konstruktor yang sama sebagai InventoryViewModel yang merupakan instance ItemDao. Perluas class dari class ViewModelProvider.Factory. Anda akan memperbaiki error terkait metode yang tidak diterapkan di langkah berikutnya.
class InventoryViewModelFactory(private val itemDao: ItemDao) : ViewModelProvider.Factory {
}
  1. Klik bohlam merah dan pilih Implement Members atau Anda dapat mengganti metode create() di dalam class ViewModelProvider.Factory sebagai berikut, yang mengambil jenis class apa pun sebagai argumen dan menampilkan objek ViewModel.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   TODO("Not yet implemented")
}
  1. Implementasikan metode create(). Periksa apakah modelClass sama dengan class InventoryViewModel dan tampilkan instance-nya. Jika tidak, berikan pengecualian.
if (modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
   @Suppress("UNCHECKED_CAST")
   return InventoryViewModel(itemDao) as T
}
throw IllegalArgumentException("Unknown ViewModel class")

Mengisi ViewModel

Di tugas ini, Anda akan mengisi class InventoryViewModel untuk menambahkan data inventaris ke database. Amati entity Item dan layar Add Item di aplikasi Inventory.

@Entity
data class Item(
   @PrimaryKey(autoGenerate = true)
   val id: Int = 0,
   @ColumnInfo(name = "name")
   val itemName: String,
   @ColumnInfo(name = "price")
   val itemPrice: Double,
   @ColumnInfo(name = "quantity")
   val quantityInStock: Int
)

85c644aced4198c5.png

Anda memerlukan nama, harga, dan stok untuk item tertentu tersebut agar dapat menambahkan entity ke database. Selanjutnya di codelab, Anda akan menggunakan layar Add Item untuk mendapatkan detail ini dari pengguna. Dalam tugas saat ini, Anda menggunakan tiga string sebagai input untuk ViewModel, mengonversinya menjadi instance entity Item, dan menyimpannya ke database menggunakan instance ItemDao. Saatnya melakukan implementasi.

  1. Di class InventoryViewModel, tambahkan fungsi private yang disebut insertItem() yang mengambil objek Item dan menambahkan data ke database dengan cara yang tidak memblokir.
private fun insertItem(item: Item) {
}
  1. Untuk berinteraksi dengan database dari thread utama, mulai coroutine dan panggil metode DAO di dalamnya. Di dalam metode insertItem(), gunakan viewModelScope.launch untuk memulai coroutine dalam ViewModelScope. Di dalam fungsi peluncuran, panggil fungsi penangguhan insert() pada itemDao yang meneruskan item. ViewModelScope adalah properti ekstensi ke class ViewModel yang otomatis membatalkan coroutine turunannya ketika ViewModel dihancurkan.
private fun insertItem(item: Item) {
   viewModelScope.launch {
       itemDao.insert(item)
   }
}

Impor kotlinx.coroutines.launch, androidx.lifecycle.viewModelScope

com.example.inventory.data.Item, jika tidak diimpor secara otomatis.

  1. Dalam class InventoryViewModel, tambahkan fungsi pribadi lainnya yang menggunakan tiga string dan menampilkan instance Item.
private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item {
   return Item(
       itemName = itemName,
       itemPrice = itemPrice.toDouble(),
       quantityInStock = itemCount.toInt()
   )
}
  1. Masih di dalam class InventoryViewModel, tambahkan fungsi publik bernama addNewItem() yang menggunakan tiga string untuk detail item. Teruskan string detail item ke fungsi getNewItemEntry() dan tetapkan nilai yang ditampilkan ke val yang bernama newItem. Lakukan panggilan ke insertItem() dengan meneruskan newItem untuk menambahkan entity baru ke database. Fungsi ini akan dipanggil dari fragmen UI untuk menambahkan detail Item ke database.
fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
   val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
   insertItem(newItem)
}

Perlu diperhatikan bahwa Anda tidak menggunakan viewModelScope.launch untuk addNewItem(), tetapi hal tersebut diperlukan di atas dalam insertItem() saat Anda memanggil metode DAO. Alasannya adalah fungsi penangguhan hanya boleh dipanggil dari coroutine atau fungsi penangguhan lainnya. Fungsi itemDao.insert(item) merupakan fungsi penangguhan.

Anda telah menambahkan semua fungsi yang diperlukan untuk menambahkan entity ke database. Pada tugas berikutnya, Anda akan memperbarui fragmen Add Item untuk menggunakan fungsi di atas.

9. Mengupdate AddItemFragment

  1. Di AddItemFragment.kt, di awal class AddItemFragment, buat private val yang disebut viewModel dari jenis InventoryViewModel. Gunakan delegasi properti Kotlin by activityViewModels() untuk membagikan ViewModel di seluruh fragmen. Anda akan memperbaiki error di langkah berikutnya.
private val viewModel: InventoryViewModel by activityViewModels {
}
  1. Di dalam lambda, panggil konstruktor InventoryViewModelFactory() lalu teruskan instance ItemDao. Gunakan instance database yang Anda buat di salah satu tugas sebelumnya untuk memanggil konstruktor itemDao.
private val viewModel: InventoryViewModel by activityViewModels {
   InventoryViewModelFactory(
       (activity?.application as InventoryApplication).database
           .itemDao()
   )
}
  1. Di bawah definisi viewModel, buat lateinit var yang disebut item dari jenis Item.
 lateinit var item: Item
  1. Layar Add Item berisi tiga kolom teks untuk mendapatkan detail item dari pengguna. Di langkah ini, Anda akan menambahkan fungsi untuk memverifikasi apakah teks di TextFields tidak kosong. Anda akan menggunakan fungsi ini untuk memverifikasi input pengguna sebelum menambah atau memperbarui entity dalam database. Validasi ini perlu dilakukan di ViewModel dan bukan di Fragment. Di class InventoryViewModel, tambahkan fungsi public berikut yang disebut isEntryValid().
fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
   if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
       return false
   }
   return true
}
  1. Di AddItemFragment.kt, di bawah fungsi onCreateView(), buat fungsi private yang disebut isEntryValid() yang menampilkan Boolean. Anda akan memperbaiki error "nilai yang ditampilkan tidak ada" di langkah berikutnya.
private fun isEntryValid(): Boolean {
}
  1. Di class AddItemFragment, implementasikan fungsi isEntryValid(). Panggil fungsi isEntryValid() pada instance viewModel, yang meneruskan teks dari tampilan teks. Tampilkan nilai fungsi viewModel.isEntryValid().
private fun isEntryValid(): Boolean {
   return viewModel.isEntryValid(
       binding.itemName.text.toString(),
       binding.itemPrice.text.toString(),
       binding.itemCount.text.toString()
   )
}
  1. Di class AddItemFragment di bawah fungsi isEntryValid(), tambahkan fungsi private lain yang disebut addNewItem() tanpa parameter serta tidak menampilkan apa pun. Di dalam fungsi, panggil isEntryValid() di dalam kondisi if.
private fun addNewItem() {
   if (isEntryValid()) {
   }
}
  1. Di dalam blok if, panggil metode addNewItem() pada instance viewModel. Teruskan detail item yang dimasukkan oleh pengguna, gunakan instance binding untuk membacanya.
if (isEntryValid()) {
   viewModel.addNewItem(
   binding.itemName.text.toString(),
   binding.itemPrice.text.toString(),
   binding.itemCount.text.toString(),
   )
}
  1. Di bawah blok if, buat val action untuk kembali ke ItemListFragment. Panggil findNavController().navigate(), dengan meneruskan action.
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)

Impor androidx.navigation.fragment.findNavController.

  1. Metode lengkapnya akan terlihat seperti berikut.
private fun addNewItem() {
       if (isEntryValid()) {
           viewModel.addNewItem(
               binding.itemName.text.toString(),
               binding.itemPrice.text.toString(),
               binding.itemCount.text.toString(),
           )
           val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
           findNavController().navigate(action)
       }
}
  1. Untuk menggabungkan semuanya, tambahkan pengendali klik ke tombol Save. Di class AddItemFragment, di atas fungsi onDestroyView(), ganti fungsi onViewCreated().
  2. Di dalam fungsi onViewCreated(), tambahkan pengendali klik ke tombol save, dan panggil addNewItem() dari fungsi tersebut.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   super.onViewCreated(view, savedInstanceState)
   binding.saveAction.setOnClickListener {
       addNewItem()
   }
}
  1. Build dan jalankan aplikasi Anda. Ketuk ikon + Fab. Di layar Add Item, tambahkan detail item dan ketuk Save. Tindakan ini menyimpan data, tetapi Anda belum dapat melihat apa pun di aplikasi. Di tugas berikutnya, Anda akan menggunakan Database Inspector untuk melihat data yang disimpan.

193c7fa9c41e0819.png

Melihat database menggunakan Database Inspector

  1. Jalankan aplikasi Anda di emulator atau perangkat terhubung yang menjalankan API level 26 atau lebih tinggi, jika Anda belum melakukannya. Database Inspector berfungsi paling baik pada emulator/perangkat yang menjalankan API level 26.
  2. Di Android Studio, pilih View > Tool Windows > Database Inspector dari panel menu.
  3. Di panel Database Inspector, pilih com.example.inventory dari menu dropdown.
  4. item_database di aplikasi Inventory akan muncul di panel Databases. Perluas node untuk item_database dan pilih Item untuk diperiksa. Jika panel Databases Anda kosong, gunakan emulator untuk menambahkan beberapa item ke database menggunakan layar Add Item.
  5. Centang kotak Live updates di Database Inspector untuk otomatis memperbarui data yang ditampilkan saat Anda berinteraksi dengan aplikasi yang berjalan di emulator atau perangkat.

4803c08f94e34118.png

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

10. Kode solusi

Kode solusi untuk codelab ini ada di repo dan cabang GitHub yang ditampilkan di bawah.

Untuk mendapatkan kode codelab ini dan membukanya di Android Studio, lakukan hal berikut.

Mendapatkan kode

  1. Klik URL yang diberikan. Tindakan ini akan membuka halaman GitHub project di browser.
  2. Di halaman GitHub project, klik tombol Code yang akan menampilkan dialog.

5b0a76c50478a73f.png

  1. Di dialog, klik tombol Download ZIP untuk menyimpan project di komputer. Tunggu download selesai.
  2. Temukan file di komputer Anda (mungkin di folder Downloads).
  3. Klik dua kali pada file ZIP untuk mengekstraknya. Tindakan ini akan membuat folder baru yang berisi file project.

Membuka project di Android Studio

  1. Mulai Android Studio.
  2. Di jendela Welcome to Android Studio, klik Open an existing Android Studio project.

36cc44fcf0f89a1d.png

Catatan: Jika Android Studio sudah terbuka, pilih opsi menu File > New > Import Project.

21f3eec988dcfbe9.png

  1. Di dialog Import Project, buka lokasi folder project yang telah diekstrak (kemungkinan ada di folder Downloads).
  2. Klik dua kali pada folder project tersebut.
  3. Tunggu Android Studio membuka project.
  4. Klik tombol Run 11c34fc5e516fb1c.png untuk mem-build dan menjalankan aplikasi. Pastikan aplikasi di-build seperti yang diharapkan.
  5. Cari file project di jendela alat Project untuk melihat cara aplikasi disiapkan.

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

12. Mempelajari lebih lanjut

Dokumentasi Developer Android

Postingan blog

Video

Dokumentasi dan artikel lainnya