1. Sebelum memulai
Anda telah mempelajari codelab sebelumnya tentang cara menggunakan library persistensi Room, yaitu lapisan abstraksi di atas database SQLite untuk menyimpan data aplikasi. Dalam codelab ini, Anda akan menambahkan lebih banyak fitur ke aplikasi Inventory dan mempelajari cara membaca, menampilkan, memperbarui, dan menghapus data dari database SQLite menggunakan Room. Anda akan menggunakan LazyColumn
untuk menampilkan data dari database dan memperbarui data secara otomatis saat data pokok dalam database berubah.
Prasyarat
- Kemampuan membuat dan berinteraksi dengan database SQLite menggunakan library Room.
- Kemampuan membuat entity, DAO, dan class database.
- Kemampuan menggunakan objek akses data (DAO) untuk memetakan fungsi Kotlin ke kueri SQL.
- Kemampuan untuk menampilkan item daftar di
LazyColumn
. - Penyelesaian codelab sebelumnya di unit ini, Mempertahankan data dengan Room.
Yang akan Anda pelajari
- Cara membaca dan menampilkan entity dari database SQLite.
- Cara memperbarui dan menghapus entity dari database SQLite menggunakan library Room.
Yang akan Anda build
- Aplikasi Inventory yang menampilkan daftar item inventaris dan dapat memperbarui, mengedit, serta menghapus item dari database aplikasi menggunakan Room.
Yang akan Anda butuhkan
- Komputer dengan Android Studio
2. Ringkasan aplikasi awal
Codelab ini menggunakan kode solusi aplikasi Inventory dari codelab sebelumnya, Mempertahankan data dengan Room sebagai kode awal. Aplikasi awal sudah menyimpan data dengan library persistensi Room. Pengguna dapat menggunakan layar Add Item untuk menambahkan data ke database aplikasi.
Dalam codelab ini, Anda akan memperluas aplikasi untuk membaca dan menampilkan data, serta memperbarui dan menghapus entity di database menggunakan library Room.
Mendownload kode awal untuk codelab ini
Untuk memulai, download kode awal:
$ 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 awal untuk codelab ini, lihat kode tersebut di GitHub.
3. Mengupdate status UI
Dalam tugas ini, Anda akan menambahkan LazyColumn
ke aplikasi untuk menampilkan data yang tersimpan dalam database.
Panduan fungsi composable HomeScreen
- Buka file
ui/home/HomeScreen.kt
dan lihat composableHomeScreen()
.
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
topBar = {
// Top app with app title
},
floatingActionButton = {
FloatingActionButton(
// onClick details
) {
Icon(
// Icon details
)
}
},
) { innerPadding ->
// Display List header and List of Items
HomeBody(
itemList = listOf(), // Empty list is being passed in for itemList
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
.fillMaxSize()
)
}
Fungsi composable ini menampilkan item berikut:
- Panel aplikasi atas dengan judul aplikasi
- Tombol tindakan mengambang (FAB) untuk penambahan item baru ke inventaris
- Fungsi composable
HomeBody()
Fungsi composable HomeBody()
menampilkan item inventaris berdasarkan daftar yang diteruskan. Sebagai bagian dari penerapan kode awal, daftar kosong (listOf()
) diteruskan ke fungsi composable HomeBody()
. Untuk meneruskan daftar inventaris ke composable ini, Anda harus mengambil data inventaris dari repositori dan meneruskannya ke HomeViewModel
.
Memberikan status UI di HomeViewModel
Saat menambahkan metode ke ItemDao
untuk mendapatkan item- getItem()
dan getAllItems()
- Anda menentukan Flow
sebagai jenis nilai yang ditampilkan. Ingat kembali bahwa Flow
mewakili aliran data generik. Dengan menampilkan Flow
, Anda hanya perlu memanggil metode dari DAO secara eksplisit satu kali untuk siklus proses tertentu. Room menangani update pada data pokok secara asinkron.
Mendapatkan data dari flow disebut mengumpulkan dari flow. Saat mengumpulkan dari flow di lapisan UI, ada beberapa hal yang perlu dipertimbangkan.
- Peristiwa siklus proses seperti perubahan konfigurasi, misalnya memutar perangkat, menyebabkan aktivitas dibuat ulang. Ini menyebabkan rekomposisi dan pengumpulan dari
Flow
Anda diulang kembali. - Anda ingin nilai di-cache sebagai status sehingga data yang ada tidak hilang di antara peristiwa siklus proses.
- Flow harus dibatalkan jika tidak ada observer yang tersisa, seperti setelah siklus proses composable berakhir.
Cara yang direkomendasikan untuk mengekspos Flow
dari ViewModel
adalah dengan StateFlow
. Penggunaan StateFlow
memungkinkan data disimpan dan diamati, terlepas dari siklus proses UI. Untuk mengonversi Flow
menjadi StateFlow
, Anda menggunakan operator stateIn
.
Operator stateIn
memiliki tiga parameter yang dijelaskan di bawah:
scope
-viewModelScope
menentukan siklus prosesStateFlow
. JikaviewModelScope
dibatalkan,StateFlow
juga akan dibatalkan.started
- Pipeline hanya boleh aktif jika UI terlihat.SharingStarted.WhileSubscribed()
digunakan untuk melakukannya. Untuk mengonfigurasi penundaan (dalam milidetik) antara hilangnya pelanggan terakhir dan penghentian coroutine berbagi, teruskanTIMEOUT_MILLIS
ke metodeSharingStarted.WhileSubscribed()
.initialValue
- Menetapkan nilai awal flow status keHomeUiState()
.
Setelah mengonversi Flow
menjadi StateFlow
, Anda dapat mengumpulkannya menggunakan metode collectAsState()
, yang mengonversi datanya menjadi State
dari jenis yang sama.
Pada langkah ini, Anda akan mengambil semua item dalam database Room sebagai API StateFlow
yang dapat diamati untuk status UI. Saat data Inventaris Room berubah, UI akan otomatis diperbarui.
- Buka file
ui/home/HomeViewModel.kt
, yang berisi konstantaTIMEOUT_MILLIS
dan class dataHomeUiState
dengan daftar item sebagai parameter konstruktor.
// No need to copy over, this code is part of starter code
class HomeViewModel : ViewModel() {
companion object {
private const val TIMEOUT_MILLIS = 5_000L
}
}
data class HomeUiState(val itemList: List<Item> = listOf())
- Di dalam class
HomeViewModel
, deklarasikanval
yang disebuthomeUiState
dari jenisStateFlow<HomeUiState>
. Anda akan segera mengatasi error inisialisasi.
val homeUiState: StateFlow<HomeUiState>
- Panggil
getAllItemsStream()
diitemsRepository
dan tetapkan kehomeUiState
yang baru saja Anda deklarasikan.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream()
Anda sekarang mendapatkan error - Referensi yang belum terselesaikan: itemsRepository. Untuk mengatasi error Referensi yang belum terselesaikan, Anda harus meneruskan objek ItemsRepository
ke HomeViewModel
.
- Tambahkan parameter konstruktor jenis
ItemsRepository
ke classHomeViewModel
.
import com.example.inventory.data.ItemsRepository
class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
- Di file
ui/AppViewModelProvider.kt
, di penginisialisasiHomeViewModel
, teruskan objekItemsRepository
seperti yang ditunjukkan.
initializer {
HomeViewModel(inventoryApplication().container.itemsRepository)
}
- Kembali ke file
HomeViewModel.kt
. Perhatikan error ketidakcocokan jenis. Untuk mengatasi hal ini, tambahkan peta transformasi seperti yang ditunjukkan di bawah.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
Android Studio masih menampilkan error ketidakcocokan jenis. Error ini terjadi karena homeUiState
adalah jenis StateFlow
dan getAllItemsStream()
menampilkan Flow
.
- Gunakan operator
stateIn
untuk mengonversiFlow
menjadiStateFlow
.StateFlow
adalah API yang dapat diamati untuk status UI, yang memungkinkan UI mengupdate dirinya sendiri.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = HomeUiState()
)
- Build aplikasi untuk memastikan tidak ada error dalam kode. Tidak akan ada perubahan visual.
4. Menampilkan data Inventaris
Dalam tugas ini, Anda mengumpulkan dan mengupdate status UI di HomeScreen
.
- Di file
HomeScreen.kt
, pada fungsi composableHomeScreen
, tambahkan parameter fungsi baru dari jenisHomeViewModel
dan lakukan inisialisasi.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun HomeScreen(
navigateToItemEntry: () -> Unit,
navigateToItemUpdate: (Int) -> Unit,
modifier: Modifier = Modifier,
viewModel: HomeViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
- Dalam fungsi composable
HomeScreen
, tambahkanval
yang disebuthomeUiState
untuk mengumpulkan status UI dariHomeViewModel
. Anda menggunakancollectAsState
()
, yang mengumpulkan nilai dariStateFlow
dan mewakili nilai terbarunya melaluiState
.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
val homeUiState by viewModel.homeUiState.collectAsState()
- Update panggilan fungsi
HomeBody()
dan teruskanhomeUiState.itemList
ke parameteritemList
.
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
)
- Jalankan aplikasi. Perhatikan bahwa daftar inventaris ditampilkan jika Anda menyimpan item di database aplikasi. Jika daftar kosong, tambahkan beberapa item inventaris ke database aplikasi.
5. Menguji database Anda
Codelab sebelumnya membahas pentingnya menguji kode Anda. Dalam tugas ini, Anda akan menambahkan beberapa pengujian unit untuk menguji kueri DAO, lalu menambahkan lebih banyak pengujian seiring progres Anda dalam codelab.
Metode yang direkomendasikan untuk menguji penerapan database adalah menulis pengujian JUnit yang berjalan di perangkat Android. Pengujian ini tidak memerlukan pembuatan aktivitas sehingga akan lebih cepat dijalankan daripada pengujian UI.
- Dalam file
build.gradle.kts (Module :app)
, perhatikan dependensi berikut untuk Espresso dan JUnit.
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
- Beralihlah ke tampilan Project, lalu klik kanan pada src > New > Directory untuk membuat set sumber pengujian bagi pengujian Anda.
- Pilih androidTest/kotlin dari pop-up New Directory.
- Buat class Kotlin bernama
ItemDaoTest.kt
. - Anotasikan class
ItemDaoTest
dengan@RunWith(AndroidJUnit4::class)
. Class Anda sekarang terlihat seperti kode contoh berikut:
package com.example.inventory
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
- Dalam class, tambahkan variabel
var
pribadi dari jenisItemDao
danInventoryDatabase
.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
- Tambahkan fungsi untuk membuat database dan menganotasinya dengan
@Before
agar dapat berjalan sebelum setiap pengujian. - Dalam metode, lakukan inisialisasi
itemDao
.
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.Before
@Before
fun createDb() {
val context: Context = ApplicationProvider.getApplicationContext()
// Using an in-memory database because the information stored here disappears when the
// process is killed.
inventoryDatabase = Room.inMemoryDatabaseBuilder(context, InventoryDatabase::class.java)
// Allowing main thread queries, just for testing.
.allowMainThreadQueries()
.build()
itemDao = inventoryDatabase.itemDao()
}
Dalam fungsi ini, Anda menggunakan database dalam memori dan tidak mempertahankannya di disk. Untuk melakukannya, gunakan fungsi inMemoryDatabaseBuilder(). Anda melakukan ini karena informasi tidak perlu dipertahankan, tetapi harus dihapus saat proses dihentikan. Anda menjalankan kueri DAO di thread utama dengan .allowMainThreadQueries()
, hanya untuk pengujian.
- Tambahkan fungsi lain untuk menutup database. Anotasikan dengan
@After
untuk menutup database dan menjalankannya setelah setiap pengujian.
import org.junit.After
import java.io.IOException
@After
@Throws(IOException::class)
fun closeDb() {
inventoryDatabase.close()
}
- Deklarasikan item di class
ItemDaoTest
yang akan digunakan database, seperti yang ditunjukkan dalam contoh kode berikut:
import com.example.inventory.data.Item
private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
- Tambahkan fungsi utilitas untuk menambahkan satu item, lalu dua item, ke database. Anda akan menggunakan fungsi ini nanti, dalam pengujian. Tandai sebagai
suspend
agar dapat berjalan di coroutine.
private suspend fun addOneItemToDb() {
itemDao.insert(item1)
}
private suspend fun addTwoItemsToDb() {
itemDao.insert(item1)
itemDao.insert(item2)
}
- Tulis pengujian untuk menyisipkan satu item ke dalam database,
insert()
. Beri nama pengujiandaoInsert_insertsItemIntoDB
dan anotasikan dengan@Test
.
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@Test
@Throws(Exception::class)
fun daoInsert_insertsItemIntoDB() = runBlocking {
addOneItemToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
}
Dalam pengujian ini, Anda menggunakan fungsi utilitas addOneItemToDb()
untuk menambahkan satu item ke database. Kemudian, Anda akan membaca item pertama dalam database. Dengan assertEquals()
, Anda membandingkan nilai yang diharapkan dengan nilai sebenarnya. Anda menjalankan pengujian dalam coroutine baru dengan runBlocking{}
. Penyiapan ini adalah alasan Anda menandai fungsi utilitas sebagai suspend
.
- Jalankan pengujian dan pastikan pengujian berhasil.
- Tulis pengujian lain untuk
getAllItems()
dari database. Beri nama pengujiandaoGetAllItems_returnsAllItemsFromDB
.
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
assertEquals(allItems[1], item2)
}
Dalam pengujian di atas, Anda menambahkan dua item ke database di dalam coroutine. Kemudian, Anda membaca kedua item tersebut dan membandingkannya dengan nilai yang diharapkan.
6. Menampilkan detail item
Dalam tugas ini, Anda akan membaca dan menampilkan detail entity di layar Item Details. Anda menggunakan status UI item, seperti nama, harga, dan jumlah dari database aplikasi inventaris, lalu menampilkannya di layar Item Details dengan composable ItemDetailsScreen
. Fungsi composable ItemDetailsScreen
ditulis sebelumnya untuk Anda dan berisi tiga composable Teks yang menampilkan detail item.
ui/item/ItemDetailsScreen.kt
Layar ini adalah bagian dari kode awal dan menampilkan detail item, yang akan Anda lihat di codelab berikutnya. Anda tidak mengerjakan layar ini dalam codelab ini. ItemDetailsViewModel.kt
adalah ViewModel
yang sesuai untuk layar ini.
- Dalam fungsi composable
HomeScreen
, perhatikan panggilan fungsiHomeBody()
.navigateToItemUpdate
diteruskan ke parameteronItemClick
, yang akan dipanggil saat Anda mengklik item mana pun dalam daftar.
// No need to copy over
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier
.padding(innerPadding)
.fillMaxSize()
)
- Buka
ui/navigation/InventoryNavGraph.kt
dan perhatikan parameternavigateToItemUpdate
di composableHomeScreen
. Parameter ini menentukan tujuan untuk navigasi sebagai layar detail item.
// No need to copy over
HomeScreen(
navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${ItemDetailsDestination.route}/${it}")
}
Bagian dari fungsi onItemClick
ini sudah diimplementasikan untuk Anda. Saat Anda mengklik item daftar, aplikasi akan membuka layar detail item.
- Klik item mana pun dalam daftar inventaris untuk melihat layar detail item dengan kolom kosong.
Untuk mengisi kolom teks dengan detail item, Anda harus mengumpulkan status UI di ItemDetailsScreen()
.
- Di
UI/Item/ItemDetailsScreen.kt
, tambahkan parameter baru ke composableItemDetailsScreen
jenisItemDetailsViewModel
dan gunakan metode factory untuk menginisialisasinya.
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.inventory.ui.AppViewModelProvider
@Composable
fun ItemDetailsScreen(
navigateToEditItem: (Int) -> Unit,
navigateBack: () -> Unit,
modifier: Modifier = Modifier,
viewModel: ItemDetailsViewModel = viewModel(factory = AppViewModelProvider.Factory)
)
- Dalam composable
ItemDetailsScreen()
, buatval
yang disebutuiState
untuk mengumpulkan status UI. GunakancollectAsState()
untuk mengumpulkanuiState
StateFlow
dan mewakili nilai terbarunya melaluiState
. Android Studio menampilkan error referensi yang belum terselesaikan.
import androidx.compose.runtime.collectAsState
val uiState = viewModel.uiState.collectAsState()
- Untuk mengatasi error, buat
val
yang disebutuiState
dari jenisStateFlow<ItemDetailsUiState>
di classItemDetailsViewModel
. - Ambil data dari repositori item dan petakan ke
ItemDetailsUiState
menggunakan fungsi ekstensitoItemDetails()
. Fungsi ekstensiItem.toItemDetails()
sudah ditulis untuk Anda sebagai bagian dari kode awal.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(itemDetails = it.toItemDetails())
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
initialValue = ItemDetailsUiState()
)
- Teruskan
ItemsRepository
keItemDetailsViewModel
untuk mengatasi errorUnresolved reference: itemsRepository
.
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
- Di
ui/AppViewModelProvider.kt
, update penginisialisasi untukItemDetailsViewModel
seperti ditunjukkan dalam cuplikan kode berikut:
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Kembali ke
ItemDetailsScreen.kt
dan perhatikan bahwa error dalam composableItemDetailsScreen()
telah diselesaikan. - Pada composable
ItemDetailsScreen()
, update panggilan fungsiItemDetailsBody()
dan teruskan argumenuiState.value
keitemUiState
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- Amati implementasi
ItemDetailsBody()
danItemInputForm()
. Anda meneruskanitem
yang saat ini dipilih dariItemDetailsBody()
keItemDetails()
.
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
//...
) {
var deleteConfirmationRequired by rememberSaveable { mutableStateOf(false) }
ItemDetails(
item = itemDetailsUiState.itemDetails.toItem(), modifier = Modifier.fillMaxWidth()
)
//...
}
- Jalankan aplikasi. Saat Anda mengklik elemen daftar pada layar Inventory, layar Item Details akan ditampilkan.
- Perhatikan bahwa layar tidak kosong lagi. Halaman ini menampilkan detail entity yang diambil dari database inventaris.
- Ketuk tombol Sell. Tidak ada yang terjadi.
Di bagian berikutnya, Anda akan menerapkan fungsi tombol Sell.
7. Menerapkan layar Detail item
ui/item/ItemEditScreen.kt
Layar Item edit sudah disediakan untuk Anda sebagai bagian dari kode awal.
Tata letak ini berisi composable kolom teks untuk mengedit detail item inventaris baru.
Kode untuk aplikasi ini masih belum berfungsi sepenuhnya. Misalnya, di layar Item Details, saat Anda mengetuk tombol Sell, Quantity in Stock tidak akan berkurang. Saat Anda mengetuk tombol Delete, aplikasi akan memunculkan dialog konfirmasi. Namun, saat Anda memilih tombol Yes, aplikasi tidak benar-benar menghapus item.
Terakhir, tombol FAB membuka layar Edit Item yang kosong.
Di bagian ini, Anda akan menerapkan fungsi tombol Sell, Delete, dan FAB.
8. Menerapkan item jual
Di bagian ini, Anda akan memperluas fitur aplikasi untuk mengimplementasikan fungsi penjualan. Update ini mencakup tugas-tugas berikut:
- Tambahkan pengujian untuk fungsi DAO guna mengupdate entity.
- Tambahkan fungsi di
ItemDetailsViewModel
untuk mengurangi jumlah dan mengupdate entity di database aplikasi. - Nonaktifkan tombol Sell jika jumlahnya nol.
- Di
ItemDaoTest.kt
, tambahkan fungsi bernamadaoUpdateItems_updatesItemsInDB()
tanpa parameter. Anotasi dengan@Test
dan@Throws(Exception::class)
.
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
- Tentukan fungsi dan buat blok
runBlocking
. PanggiladdTwoItemsToDb()
di dalamnya.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
}
- Perbarui kedua entity dengan nilai yang berbeda, yang memanggil
itemDao.update
.
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
- Ambil entity dengan
itemDao.getAllItems()
. Bandingkan dengan entity yang diupdate dan nyatakan.
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
- Pastikan fungsi yang sudah selesai terlihat seperti berikut:
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
}
- Jalankan pengujian dan pastikan pengujian berhasil.
Tambahkan fungsi di ViewModel
- Di
ItemDetailsViewModel.kt
, dalam classItemDetailsViewModel
, tambahkan fungsi bernamareduceQuantityByOne()
tanpa parameter.
fun reduceQuantityByOne() {
}
- Di dalam fungsi, mulai coroutine dengan
viewModelScope.launch{}
.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
viewModelScope.launch {
}
- Di dalam blok
launch
, buatval
bernamacurrentItem
dan tetapkan keuiState.value.toItem()
.
val currentItem = uiState.value.toItem()
uiState.value
adalah jenis ItemUiState
. Anda mengonversinya menjadi jenis entity Item
dengan fungsi ekstensi toItem
()
.
- Tambahkan pernyataan
if
untuk memastikanquality
lebih besar dari0
. - Panggil
updateItem()
padaitemsRepository
dan teruskancurrentItem
yang diupdate. Gunakancopy()
untuk mengupdate nilaiquantity
sehingga fungsi terlihat seperti berikut:
fun reduceQuantityByOne() {
viewModelScope.launch {
val currentItem = uiState.value.itemDetails.toItem()
if (currentItem.quantity > 0) {
itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
}
}
}
- Kembali ke
ItemDetailsScreen.kt
. - Pada composable
ItemDetailsScreen
, buka panggilan fungsiItemDetailsBody()
. - Di lambda
onSellItem
, panggilviewModel.reduceQuantityByOne()
.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- Jalankan aplikasi.
- Di layar Inventory, klik elemen daftar. Saat layar Item Details ditampilkan, ketuk Sell dan perhatikan bahwa nilai jumlahnya berkurang satu.
- Di layar Item Details, terus ketuk tombol Sell hingga jumlahnya nol.
Setelah jumlahnya mencapai nol, ketuk Sell lagi. Tidak ada perubahan visual karena fungsi reduceQuantityByOne()
memeriksa apakah jumlahnya lebih besar dari nol sebelum memperbarui jumlah.
Untuk memberikan masukan yang lebih baik kepada pengguna, sebaiknya nonaktifkan tombol Sell saat tidak ada item yang dijual.
- Di class
ItemDetailsViewModel
, tetapkan nilaioutOfStock
berdasarkanit
.quantity
dalam transformasimap
.
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
}.stateIn(
//...
)
- Jalankan aplikasi Anda. Perhatikan bahwa aplikasi menonaktifkan tombol Sell saat jumlah yang tersedia nol.
Selamat, Anda telah berhasil menerapkan fitur item Sell ke aplikasi.
Menghapus entity item
Seperti tugas sebelumnya, Anda harus memperluas fitur aplikasi lebih jauh dengan menerapkan fungsi hapus. Fitur ini jauh lebih mudah diterapkan daripada fitur penjualan. Proses ini melibatkan tugas berikut:
- Tambahkan pengujian untuk kueri DAO hapus.
- Tambahkan fungsi di class
ItemDetailsViewModel
untuk menghapus entity dari database - Update composable
ItemDetailsBody
.
Menambahkan pengujian DAO
- Di
ItemDaoTest.kt
, tambahkan pengujian bernamadaoDeleteItems_deletesAllItemsFromDB()
.
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
- Luncurkan coroutine dengan
runBlocking {}
.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
- Tambahkan dua item ke database dan panggil
itemDao.delete()
pada dua item tersebut untuk menghapusnya dari database.
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
- Ambil entity dari database dan pastikan daftar tersebut kosong. Pengujian yang sudah selesai akan terlihat seperti berikut.
import org.junit.Assert.assertTrue
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
val allItems = itemDao.getAllItems().first()
assertTrue(allItems.isEmpty())
}
Menambahkan fungsi hapus di ItemDetailsViewModel
- Di
ItemDetailsViewModel
, tambahkan fungsi baru bernamadeleteItem()
yang tidak menggunakan parameter dan tidak menampilkan apa pun. - Di dalam fungsi
deleteItem()
, tambahkan panggilan fungsiitemsRepository.deleteItem()
dan teruskanuiState.value.
toItem
()
.
suspend fun deleteItem() {
itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}
Dalam fungsi ini, Anda akan mengonversi uiState
dari jenis itemDetails
menjadi jenis entity Item
menggunakan fungsi ekstensi toItem
()
.
- Di composable
ui/item/ItemDetailsScreen
, tambahkanval
bernamacoroutineScope
dan tetapkan kerememberCoroutineScope()
. Pendekatan ini menampilkan cakupan coroutine yang terikat ke komposisi tempatnya dipanggil (composableItemDetailsScreen
).
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Scroll ke fungsi
ItemDetailsBody()
. - Luncurkan coroutine dengan
coroutineScope
di dalam lambdaonDelete
. - Di dalam blok
launch
, panggil metodedeleteItem()
diviewModel
.
import kotlinx.coroutines.launch
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
modifier = modifier.padding(innerPadding)
)
- Setelah menghapus item, kembali ke layar inventaris.
- Panggil
navigateBack()
setelah panggilan fungsideleteItem()
.
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
navigateBack()
}
- Masih dalam file
ItemDetailsScreen.kt
, scroll ke fungsiItemDetailsBody()
.
Fungsi ini adalah bagian dari kode awal. Composable ini menampilkan dialog pemberitahuan untuk mendapatkan konfirmasi pengguna sebelum menghapus item dan memanggil fungsi deleteItem()
saat Anda mengetuk Yes.
// No need to copy over
@Composable
private fun ItemDetailsBody(
itemUiState: ItemUiState,
onSellItem: () -> Unit,
onDelete: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
/*...*/
) {
//...
if (deleteConfirmationRequired) {
DeleteConfirmationDialog(
onDeleteConfirm = {
deleteConfirmationRequired = false
onDelete()
},
//...
)
}
}
}
Saat Anda mengetuk No, aplikasi akan menutup dialog pemberitahuan. Fungsi showConfirmationDialog()
menampilkan pemberitahuan berikut:
- Jalankan aplikasi.
- Pilih elemen daftar di layar Inventory.
- Di layar Item Details, ketuk Delete.
- Ketuk Yes pada dialog pemberitahuan, dan aplikasi akan membuka kembali layar Inventory.
- Pastikan entity yang dihapus tidak ada lagi di database aplikasi.
Selamat, Anda telah berhasil menerapkan fitur hapus.
Mengedit entity item
Serupa dengan bagian sebelumnya, di bagian ini, Anda akan menambahkan peningkatan fitur lainnya ke aplikasi yang mengedit entity item.
Berikut adalah langkah-langkah cepat untuk mengedit entity dalam database aplikasi:
- Tambahkan pengujian ke kueri DAO item dapatkan pengujian.
- Isi kolom teks dan layar Edit Item dengan detail entity.
- Perbarui entity dalam database menggunakan Room.
Menambahkan pengujian DAO
- Di
ItemDaoTest.kt
, tambahkan pengujian bernamadaoGetItem_returnsItemFromDB()
.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
- Tentukan fungsi. Di dalam coroutine, tambahkan satu item ke database.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
}
- Ambil entity dari database menggunakan fungsi
itemDao.getItem()
dan tetapkan keval
bernamaitem
.
val item = itemDao.getItem(1)
- Bandingkan nilai sebenarnya dengan nilai yang diambil, lalu nyatakan menggunakan
assertEquals()
. Pengujian yang telah selesai akan terlihat seperti berikut:
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
val item = itemDao.getItem(1)
assertEquals(item.first(), item1)
}
- Jalankan pengujian dan pastikan pengujian berhasil.
Mengisi kolom teks
Jika Anda menjalankan aplikasi, buka layar Item Details, lalu klik FAB. Anda dapat melihat bahwa judul layar sekarang adalah Edit Item. Namun, semua kolom teks kosong. Di langkah ini, Anda akan mengisi kolom teks di layar Edit Item dengan detail entity.
- Di
ItemDetailsScreen.kt
, scroll ke composableItemDetailsScreen
. - Di
FloatingActionButton()
, ubah argumenonClick
untuk menyertakanuiState.value.itemDetails.id
, yang merupakanid
dari entity yang dipilih. Anda dapat menggunakanid
ini untuk mengambil detail entity.
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
modifier = /*...*/
)
- Di class
ItemEditViewModel
, tambahkan blokinit
.
init {
}
- Di dalam blok
init
, luncurkan coroutine denganviewModelScope
.
launch
.
import kotlinx.coroutines.launch
viewModelScope.launch { }
- Di dalam blok
launch
, ambil detail entity denganitemsRepository.getItemStream(itemId)
.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
init {
viewModelScope.launch {
itemUiState = itemsRepository.getItemStream(itemId)
.filterNotNull()
.first()
.toItemUiState(true)
}
}
Di blok peluncuran ini, Anda akan menambahkan filter untuk menampilkan flow yang hanya berisi nilai yang bukan null. Dengan toItemUiState()
, Anda mengonversi entity item
menjadi ItemUiState
. Anda meneruskan nilai actionEnabled
sebagai true
untuk mengaktifkan tombol Save.
Untuk mengatasi error Unresolved reference: itemsRepository
, Anda harus meneruskan ItemsRepository
sebagai dependensi ke model tampilan.
- Tambahkan parameter konstruktor ke class
ItemEditViewModel
:
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
)
- Pada file
AppViewModelProvider.kt
, di penginisialisasiItemEditViewModel
, tambahkan objekItemsRepository
sebagai argumen.
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- Jalankan aplikasi.
- Buka Item Details, lalu ketuk FAB.
- Perhatikan bahwa kolom diisi dengan detail item.
- Edit jumlah stok atau kolom lainnya, lalu ketuk Save.
Tidak ada yang terjadi. Ini karena Anda tidak memperbarui entity dalam database aplikasi. Anda dapat memperbaikinya di bagian berikutnya.
Mengupdate entity menggunakan Room
Di tugas terakhir ini, Anda menambahkan bagian akhir kode untuk mengimplementasikan fungsi update. Anda akan menentukan fungsi yang diperlukan di ViewModel dan menggunakannya di ItemEditScreen
.
Ini waktunya membuat kode lagi.
- Di class
ItemEditViewModel
, tambahkan fungsi bernamaupdateUiState()
yang menggunakan objekItemUiState
dan tidak menampilkan apa pun. Fungsi ini mengupdateitemUiState
dengan nilai baru yang dimasukkan pengguna.
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
Dalam fungsi ini, Anda menetapkan itemDetails
yang diteruskan ke itemUiState
dan mengupdate nilai isEntryValid
. Aplikasi akan mengaktifkan tombol Save jika itemDetails
adalah true
. Anda menetapkan nilai ini ke true
hanya jika input yang dimasukkan pengguna valid.
- Buka file
ItemEditScreen.kt
. - Di composable
ItemEditScreen
, scroll ke bawah ke panggilan fungsiItemEntryBody()
. - Tetapkan nilai argumen
onItemValueChange
ke fungsi baruupdateUiState
.
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = modifier.padding(innerPadding)
)
- Jalankan aplikasi.
- Buka layar Edit Item.
- Buat salah satu nilai entity kosong sehingga tidak valid. Perhatikan cara tombol Save dinonaktifkan secara otomatis.
- Kembali ke class
ItemEditViewModel
dan tambahkan fungsisuspend
bernamaupdateItem()
yang tidak memerlukan apa pun. Anda menggunakan fungsi ini untuk menyimpan entity yang telah diupdate ke database Room.
suspend fun updateItem() {
}
- Di dalam fungsi
getUpdatedItemEntry()
, tambahkan kondisiif
untuk memvalidasi input pengguna dengan menggunakan fungsivalidateInput()
. - Lakukan panggilan ke fungsi
updateItem()
diitemsRepository
, yang meneruskanitemUiState.itemDetails.
toItem
()
. Entity yang dapat ditambahkan ke database Room harus berjenisItem
. Fungsi yang sudah selesai akan terlihat seperti berikut:
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
- Kembali ke composable
ItemEditScreen
. Anda memerlukan cakupan coroutine untuk memanggil fungsiupdateItem()
. Buat val bernamacoroutineScope
dan tetapkan kerememberCoroutineScope()
.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
- Pada panggilan fungsi
ItemEntryBody()
, update argumen fungsionSaveClick
untuk memulai coroutine dalamcoroutineScope
. - Di dalam blok
launch
, panggilupdateItem()
padaviewModel
dan buka kembali.
import kotlinx.coroutines.launch
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
Panggilan fungsi ItemEntryBody()
yang sudah selesai akan terlihat seperti berikut:
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- Jalankan aplikasi dan coba edit item inventaris. Anda sekarang dapat mengedit item apa pun di database aplikasi Inventory.
Selamat atas pembuatan aplikasi pertama Anda yang menggunakan Room untuk mengelola database.
9. Kode solusi
Kode solusi untuk codelab ini ada di repo dan cabang GitHub yang ditampilkan di bawah:
10. Mempelajari lebih lanjut
Dokumentasi Developer Android
- Mendebug database dengan Database Inspector
- Menyimpan data di dalam database lokal menggunakan Room
- Menguji dan men-debug database | Android Developers
Referensi Kotlin