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.
Prasyarat
- Kemampuan untuk membangun antarmuka pengguna (UI) dasar untuk aplikasi Android menggunakan Jetpack Compose.
- Kemampuan untuk menggunakan composable seperti
Text
,Icon
,IconButton
, danLazyColumn
. - 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 menggunakanViewModelProvider.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.
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
- Buka project dengan kode awal di Android Studio.
- 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.
- Perhatikan bahwa aplikasi tidak menampilkan data inventaris.
- 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.
Masalah dengan kode awal
- Di layar Add Item, masukkan detail item seperti nama, harga, dan jumlah Item.
- 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.
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.
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.
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.
Menambahkan dependensi Room
Dalam tugas ini, Anda akan menambahkan library komponen Room yang diperlukan ke file Gradle Anda.
- Buka file gradle level modul
build.gradle.kts (Module: InventoryApp.app)
. - 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.
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.
- Buka kode awal di Android Studio.
- Buka paket
data
pada paket dasarcom.example.inventory
. - Di dalam paket
data
, buka class KotlinItem
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
atauvar
. - Class data tidak boleh berupa
abstract
,open
, atausealed
.
Untuk mempelajari class Data lebih lanjut, lihat dokumentasi Class data.
- Awali definisi class
Item
dengan kata kuncidata
untuk mengonversinya menjadi class data.
data class Item(
val id: Int,
val name: String,
val price: Double,
val quantity: Int
)
- Di atas deklarasi class
Item
, beri anotasi pada class data dengan@Entity
. Gunakan argumentableName
untuk menetapkanitems
sebagai nama tabel SQLite.
import androidx.room.Entity
@Entity(tableName = "items")
data class Item(
...
)
- Anotasikan properti
id
dengan@PrimaryKey
untuk menjadikanid
sebagai kunci utama. Kunci utama adalah ID untuk mengidentifikasi setiap catatan/entri dalam tabelItem
Anda secara unik
import androidx.room.PrimaryKey
@Entity(tableName = "items")
data class Item(
@PrimaryKey
val id: Int,
...
)
- Tetapkan nilai default sebesar
0
padaid
, yang diperlukanid
untuk membuat nilaiid
secara otomatis. - Tambahkan parameter
autoGenerate
ke anotasi@PrimaryKey
untuk menentukan apakah kolom kunci utama harus dibuat secara otomatis. JikaautoGenerate
disetel ketrue
, 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.
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.
Selesaikan langkah-langkah berikut untuk menerapkan DAO item di aplikasi Anda:
- Di paket
data
, buat antarmuka KotlinItemDao.kt
.
- Anotasi antarmuka
ItemDao
dengan@Dao
.
import androidx.room.Dao
@Dao
interface ItemDao {
}
- Di dalam isi antarmuka, tambahkan anotasi
@Insert
. - Di bawah
@Insert
, tambahkan fungsiinsert()
yang menggunakan instance classEntity
item
sebagai argumennya. - 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.
- Tambahkan argumen
onConflict
dan tetapkan nilaiOnConflictStrategy.
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.
- Tambahkan fungsi baru dengan anotasi
@Update
yang menggunakanItem
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.
- Serupa dengan metode
insert()
, tandai fungsi ini dengan kata kuncisuspend
.
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.
- Tulis kueri SQLite untuk mengambil item tertentu dari tabel item berdasarkan
id
yang diberikan. Kode berikut memberikan contoh kueri yang memilih semua kolom dariitems
, denganid
yang cocok dengan nilai tertentu danid
adalah ID unik.
Contoh:
// Example, no need to copy over
SELECT * from items WHERE id = 1
- Tambahkan anotasi
@Query
. - Gunakan kueri SQLite dari langkah sebelumnya sebagai parameter string ke anotasi
@Query
. - 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")
- Setelah anotasi
@Query
, tambahkan fungsigetItem()
yang menggunakan argumenInt
dan menampilkanFlow<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.
- Tambahkan
@Query
dengan fungsigetAllItems()
. - Minta kueri SQLite untuk menampilkan semua kolom dari tabel
item
yang diurutkan dalam urutan menaik. - Minta
getAllItems()
menampilkan daftar entityItem
sebagaiFlow
.Room
terus memperbaruiFlow
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>>
}
- 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 memperluasRoomDatabase
. Class abstrak baru yang Anda tentukan berfungsi sebagai holder database. Class yang Anda tentukan bersifat abstrak, karenaRoom
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
, danRoom
akan menghasilkan implementasinya untuk Anda. - Anda hanya memerlukan satu instance
RoomDatabase
untuk seluruh aplikasi, sehingga jadikanRoomDatabase
sebuah singleton. - Gunakan
Room.databaseBuilder
Room
untuk membuat database (item_database
) hanya jika tidak ada. Jika tidak, tampilkan database yang ada.
Membuat Database
- Di paket
data
, buat class KotlinInventoryDatabase.kt
. - Di file
InventoryDatabase.kt
, buat classInventoryDatabase
sebagai classabstract
yang memperluasRoomDatabase
. - 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.
- Tentukan
Item
sebagai satu-satunya class dengan daftarentities
. - Setel
version
sebagai1
. Setiap kali mengubah skema tabel database, Anda harus meningkatkan nomor versinya. - Setel
exportSchema
kefalse
agar tidak menyimpan cadangan histori versi skema.
@Database(entities = [Item::class], version = 1, exportSchema = false)
- Di dalam isi class, deklarasikan fungsi abstrak yang menampilkan
ItemDao
sehingga database mengetahui DAO.
abstract fun itemDao(): ItemDao
- 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 {}
- Di dalam objek
companion
, deklarasikan variabel nullable pribadiInstance
untuk database lalu inisialisasikan kenull
.
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 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
- Di bawah
Instance
, saat masih berada di dalam objekcompanion
, tentukan metodegetDatabase()
dengan parameterContext
yang diperlukan builder database. - Tampilkan jenis
InventoryDatabase
. Pesan error muncul karenagetDatabase()
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.
- Dalam
getDatabase()
, tampilkan variabelInstance
. Atau, jikaInstance
adalah null, lakukan inisialisasi di dalam bloksynchronized{}
. Gunakan operator elvis (?:
) untuk melakukannya. - Teruskan
this
, objek pendamping. Anda dapat memperbaiki error tersebut di langkah berikutnya.
return Instance ?: synchronized(this) { }
- 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()
- Di dalam blok
synchronized
, gunakan builder database untuk mendapatkan database. Teruskan konteks aplikasi, class database, serta nama untuk database-item_database
keRoom.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.
- Tambahkan strategi migrasi yang diperlukan ke builder. Gunakan
.
fallbackToDestructiveMigration()
.
.fallbackToDestructiveMigration()
- Untuk membuat instance database, panggil
.build()
. Panggilan ini akan menghapus error Android Studio.
.build()
- Setelah
build()
, tambahkan blokalso
dan tetapkanInstance = it
untuk mempertahankan referensi ke instance db yang baru dibuat.
.also { Instance = it }
- Di akhir blok
synchronized
, tampilkaninstance
. 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 }
}
}
}
}
- 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.
- Buka file
ItemsRepository.kt
pada paketdata
. - 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)
}
- Buka file
OfflineItemsRepository.kt
pada paketdata
. - Teruskan parameter konstruktor jenis
ItemDao
.
class OfflineItemsRepository(private val itemDao: ItemDao) : ItemsRepository
- Di class
OfflineItemsRepository
, ganti fungsi yang ditentukan di antarmukaItemsRepository
dan panggil fungsi yang sesuai dariItemDao
.
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
.
- Buka file
AppContainer.kt
pada paketdata
. - Teruskan instance
ItemDao()
ke konstruktorOfflineItemsRepository
. - Buat instance database dengan memanggil
getDatabase()
pada classInventoryDatabase
yang meneruskan konteks, lalu memanggil.itemDao()
untuk membuat instanceDao
.
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 UIItemUiState
menjadi jenis entityItem
. - Fungsi ekstensi
Item.toItemUiState()
mengonversi objek entity RoomItem
menjadi jenis status UIItemUiState
. - Fungsi ekstensi
Item.toItemDetails()
mengonversi objek entity RoomItem
menjadiItemDetails
.
// 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.
- Perhatikan fungsi pribadi
validateInput()
di classItemEntryViewModel
.
// 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.
- Buka class
ItemEntryViewModel
dan tambahkan parameter konstruktor defaultprivate
dari jenisItemsRepository
.
import com.example.inventory.data.ItemsRepository
class ItemEntryViewModel(private val itemsRepository: ItemsRepository) : ViewModel() {
}
- Update
initializer
untuk model tampilan entri item diui/AppViewModelProvider.kt
dan teruskan instance repositori sebagai parameter.
object AppViewModelProvider {
val Factory = viewModelFactory {
// Other Initializers
// Initializer for ItemEntryViewModel
initializer {
ItemEntryViewModel(inventoryApplication().container.itemsRepository)
}
//...
}
}
- Buka file
ItemEntryViewModel.kt
dan di akhir classItemEntryViewModel
, lalu tambahkan fungsi penangguhan bernamasaveItem()
untuk menyisipkan item ke dalam database Room. Fungsi ini menambahkan data ke database dengan cara yang tidak memblokir.
suspend fun saveItem() {
}
- Di dalam fungsi, periksa apakah
itemUiState
valid, lalu konversikan ke jenisItem
agar Room dapat memahami data. - Panggil
insertItem()
padaitemsRepository
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()
- Dalam file
ui/item/ItemEntryScreen.kt
, composableItemEntryBody()
diterapkan sebagian untuk Anda sebagai bagian dari kode awal. Lihat composableItemEntryBody()
dalam panggilan fungsiItemEntryScreen()
.
// 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()
)
- 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).
- Lihat implementasi fungsi composable
ItemInputForm()
dan perhatikan parameter fungsionValueChange
. Anda memperbarui nilaiitemDetails
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.
- Di
ItemEntryScreen.kt
, dalam fungsi composableItemEntryScreen
, buatval
bernamacoroutineScope
dengan fungsi composablerememberCoroutineScope()
.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Update panggilan fungsi
ItemEntryBody
()
dan luncurkan coroutine dalam lambdaonSaveClick
.
ItemEntryBody(
// ...
onSaveClick = {
coroutineScope.launch {
}
},
modifier = modifier.padding(innerPadding)
)
- Lihat implementasi fungsi
saveItem()
di fileItemEntryViewModel.kt
untuk memeriksa apakahitemUiState
valid, mengonversiitemUiState
menjadi jenisItem
, dan menyisipkannya dalam database menggunakanitemsRepository.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())
}
}
- Di
ItemEntryScreen.kt
, dalam fungsi composableItemEntryScreen
, di dalam coroutine, panggilviewModel.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.
- Bangun dan jalankan aplikasi Anda.
- Ketuk + FAB.
- Di layar Add Item, tambahkan detail item dan ketuk Save. Perhatikan bahwa mengetuk tombol Save tidak akan menutup layar Add Item.
- Di lambda
onSaveClick
, tambahkan panggilan kenavigateBack()
setelah panggilan keviewModel.saveItem()
untuk membuka kembali layar sebelumnya. FungsiItemEntryBody()
terlihat seperti kode berikut:
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.saveItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- 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.
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.
- Jalankan aplikasi Anda di emulator atau perangkat terhubung yang menjalankan API level 26 atau lebih tinggi, jika Anda belum melakukannya.
- Di Android Studio, pilih View > Tool Windows > App Inspection dari panel menu.
- Pilih tab Database Inspector.
- Di panel Database Inspector, pilih
com.example.inventory
dari menu dropdown jika belum dipilih. item_database di aplikasi Inventory akan muncul di panel Databases.
- 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.
- Centang kotak Live updates di Database Inspector untuk otomatis memperbarui data yang ditampilkan saat Anda berinteraksi dengan aplikasi yang berjalan di emulator atau perangkat.
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
- Menyimpan data di dalam database lokal menggunakan Room
- androidx.room
- Mendebug database dengan Database Inspector
Postingan blog
Video
Dokumentasi dan artikel lainnya