Membaca dan memperbarui data dengan Room

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.

Layar tambahkan item dengan kolom teks kosong

Inventaris kosong layar ponsel

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.

Layar ponsel dengan item inventaris

Panduan fungsi composable HomeScreen

  • Buka file ui/home/HomeScreen.kt dan lihat composable HomeScreen().
@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 adc468afa54b6e70.png
  • 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 proses StateFlow. Jika viewModelScope 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, teruskan TIMEOUT_MILLIS ke metode SharingStarted.WhileSubscribed().
  • initialValue - Menetapkan nilai awal flow status ke HomeUiState().

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.

  1. Buka file ui/home/HomeViewModel.kt, yang berisi konstanta TIMEOUT_MILLIS dan class data HomeUiState 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())
  1. Di dalam class HomeViewModel, deklarasikan val yang disebut homeUiState dari jenis StateFlow<HomeUiState>. Anda akan segera mengatasi error inisialisasi.
val homeUiState: StateFlow<HomeUiState>
  1. Panggil getAllItemsStream() di itemsRepository dan tetapkan ke homeUiState 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.

  1. Tambahkan parameter konstruktor jenis ItemsRepository ke class HomeViewModel.
import com.example.inventory.data.ItemsRepository

class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
  1. Di file ui/AppViewModelProvider.kt, di penginisialisasi HomeViewModel, teruskan objek ItemsRepository seperti yang ditunjukkan.
initializer {
    HomeViewModel(inventoryApplication().container.itemsRepository)
}
  1. 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.

  1. Gunakan operator stateIn untuk mengonversi Flow menjadi StateFlow. 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()
        )
  1. 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.

  1. Di file HomeScreen.kt, pada fungsi composable HomeScreen, tambahkan parameter fungsi baru dari jenis HomeViewModel 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)
)
  1. Dalam fungsi composable HomeScreen, tambahkan val yang disebut homeUiState untuk mengumpulkan status UI dari HomeViewModel. Anda menggunakan collectAsState(), yang mengumpulkan nilai dari StateFlow dan mewakili nilai terbarunya melalui State.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

val homeUiState by viewModel.homeUiState.collectAsState()
  1. Update panggilan fungsi HomeBody() dan teruskan homeUiState.itemList ke parameter itemList.
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier.padding(innerPadding)
)
  1. Jalankan aplikasi. Perhatikan bahwa daftar inventaris ditampilkan jika Anda menyimpan item di database aplikasi. Jika daftar kosong, tambahkan beberapa item inventaris ke database aplikasi.

Layar ponsel dengan item inventaris

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.

  1. 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")
  1. Beralihlah ke tampilan Project, lalu klik kanan pada src > New > Directory untuk membuat set sumber pengujian bagi pengujian Anda.

e53b0f0e0b6aba29.png

  1. Pilih androidTest/kotlin dari pop-up New Directory.

860b7e1af5f116a.png

  1. Buat class Kotlin bernama ItemDaoTest.kt.
  2. 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 {
}
  1. Dalam class, tambahkan variabel var pribadi dari jenis ItemDao dan InventoryDatabase.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao

private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
  1. Tambahkan fungsi untuk membuat database dan menganotasinya dengan @Before agar dapat berjalan sebelum setiap pengujian.
  2. 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.

  1. 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()
}
  1. 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)
  1. 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)
}
  1. Tulis pengujian untuk menyisipkan satu item ke dalam database, insert(). Beri nama pengujian daoInsert_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.

  1. Jalankan pengujian dan pastikan pengujian berhasil.

cd95648114520f13.png

6521e8595bb33a91.png

  1. Tulis pengujian lain untuk getAllItems() dari database. Beri nama pengujian daoGetAllItems_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.

a5009ad021b830ff.png

  1. Dalam fungsi composable HomeScreen, perhatikan panggilan fungsi HomeBody(). navigateToItemUpdate diteruskan ke parameter onItemClick, 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()
)
  1. Buka ui/navigation/InventoryNavGraph.kt dan perhatikan parameter navigateToItemUpdate di composable HomeScreen. 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.

  1. Klik item mana pun dalam daftar inventaris untuk melihat layar detail item dengan kolom kosong.

Layar detail item dengan kolom kosong

Untuk mengisi kolom teks dengan detail item, Anda harus mengumpulkan status UI di ItemDetailsScreen().

  1. Di UI/Item/ItemDetailsScreen.kt, tambahkan parameter baru ke composable ItemDetailsScreen jenis ItemDetailsViewModel 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)
)
  1. Dalam composable ItemDetailsScreen(), buat val yang disebut uiState untuk mengumpulkan status UI. Gunakan collectAsState() untuk mengumpulkan uiState StateFlow dan mewakili nilai terbarunya melalui State. Android Studio menampilkan error referensi yang belum terselesaikan.
import androidx.compose.runtime.collectAsState

val uiState = viewModel.uiState.collectAsState()
  1. Untuk mengatasi error, buat val yang disebut uiState dari jenis StateFlow<ItemDetailsUiState> di class ItemDetailsViewModel.
  2. Ambil data dari repositori item dan petakan ke ItemDetailsUiState menggunakan fungsi ekstensi toItemDetails(). Fungsi ekstensi Item.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()
             )
  1. Teruskan ItemsRepository ke ItemDetailsViewModel untuk mengatasi error Unresolved reference: itemsRepository.
class ItemDetailsViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
    ) : ViewModel() {
  1. Di ui/AppViewModelProvider.kt, update penginisialisasi untuk ItemDetailsViewModel seperti ditunjukkan dalam cuplikan kode berikut:
initializer {
    ItemDetailsViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Kembali ke ItemDetailsScreen.kt dan perhatikan bahwa error dalam composable ItemDetailsScreen() telah diselesaikan.
  2. Pada composable ItemDetailsScreen(), update panggilan fungsi ItemDetailsBody() dan teruskan argumen uiState.value ke itemUiState.
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = {  },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Amati implementasi ItemDetailsBody() dan ItemInputForm(). Anda meneruskan item yang saat ini dipilih dari ItemDetailsBody() ke ItemDetails().
// 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()
         )

      //...
    }
  1. Jalankan aplikasi. Saat Anda mengklik elemen daftar pada layar Inventory, layar Item Details akan ditampilkan.
  2. Perhatikan bahwa layar tidak kosong lagi. Halaman ini menampilkan detail entity yang diambil dari database inventaris.

Layar detail item dengan detail item yang diisi

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

Layar edit item dengan kolom kosong

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.

pop-up konfirmasi penghapusan item

Terakhir, tombol FAB be6c7ed4ac207351.png membuka layar Edit Item yang kosong.

Layar edit item dengan kolom 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.
  1. Di ItemDaoTest.kt, tambahkan fungsi bernama daoUpdateItems_updatesItemsInDB() tanpa parameter. Anotasi dengan @Test dan @Throws(Exception::class).
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
  1. Tentukan fungsi dan buat blok runBlocking. Panggil addTwoItemsToDb() di dalamnya.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
    addTwoItemsToDb()
}
  1. 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))
  1. 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))
  1. 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))
}
  1. Jalankan pengujian dan pastikan pengujian berhasil.

Tambahkan fungsi di ViewModel

  1. Di ItemDetailsViewModel.kt, dalam class ItemDetailsViewModel, tambahkan fungsi bernama reduceQuantityByOne() tanpa parameter.
fun reduceQuantityByOne() {
}
  1. Di dalam fungsi, mulai coroutine dengan viewModelScope.launch{}.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope

viewModelScope.launch {
}
  1. Di dalam blok launch, buat val bernama currentItem dan tetapkan ke uiState.value.toItem().
val currentItem = uiState.value.toItem()

uiState.value adalah jenis ItemUiState. Anda mengonversinya menjadi jenis entity Item dengan fungsi ekstensi toItem().

  1. Tambahkan pernyataan if untuk memastikan quality lebih besar dari 0.
  2. Panggil updateItem() pada itemsRepository dan teruskan currentItem yang diupdate. Gunakan copy() untuk mengupdate nilai quantity 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))
       }
    }
}
  1. Kembali ke ItemDetailsScreen.kt.
  2. Pada composable ItemDetailsScreen, buka panggilan fungsi ItemDetailsBody().
  3. Di lambda onSellItem, panggil viewModel.reduceQuantityByOne().
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Jalankan aplikasi.
  2. Di layar Inventory, klik elemen daftar. Saat layar Item Details ditampilkan, ketuk Sell dan perhatikan bahwa nilai jumlahnya berkurang satu.

Layar detail item mengurangi kuantitas sebesar satu saat tombol jual diketuk

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

Layar detail item dengan jumlah stok 0

Untuk memberikan masukan yang lebih baik kepada pengguna, sebaiknya nonaktifkan tombol Sell saat tidak ada item yang dijual.

  1. Di class ItemDetailsViewModel, tetapkan nilai outOfStock berdasarkan it.quantity dalam transformasi map.
val uiState: StateFlow<ItemDetailsUiState> =
    itemsRepository.getItemStream(itemId)
        .filterNotNull()
        .map {
            ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
        }.stateIn(
            //...
        )
  1. Jalankan aplikasi Anda. Perhatikan bahwa aplikasi menonaktifkan tombol Sell saat jumlah yang tersedia nol.

Layar detail item dengan tombol jual diaktifkan

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

  1. Di ItemDaoTest.kt, tambahkan pengujian bernama daoDeleteItems_deletesAllItemsFromDB().
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
  1. Luncurkan coroutine dengan runBlocking {}.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
  1. Tambahkan dua item ke database dan panggil itemDao.delete() pada dua item tersebut untuk menghapusnya dari database.
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
  1. 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

  1. Di ItemDetailsViewModel, tambahkan fungsi baru bernama deleteItem() yang tidak menggunakan parameter dan tidak menampilkan apa pun.
  2. Di dalam fungsi deleteItem(), tambahkan panggilan fungsi itemsRepository.deleteItem() dan teruskan uiState.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().

  1. Di composable ui/item/ItemDetailsScreen, tambahkan val bernama coroutineScope dan tetapkan ke rememberCoroutineScope(). Pendekatan ini menampilkan cakupan coroutine yang terikat ke komposisi tempatnya dipanggil (composable ItemDetailsScreen).
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Scroll ke fungsi ItemDetailsBody().
  2. Luncurkan coroutine dengan coroutineScope di dalam lambda onDelete.
  3. Di dalam blok launch, panggil metode deleteItem() di viewModel.
import kotlinx.coroutines.launch

ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = {
        coroutineScope.launch {
           viewModel.deleteItem()
    }
    modifier = modifier.padding(innerPadding)
)
  1. Setelah menghapus item, kembali ke layar inventaris.
  2. Panggil navigateBack() setelah panggilan fungsi deleteItem().
onDelete = {
    coroutineScope.launch {
        viewModel.deleteItem()
        navigateBack()
    }
  1. Masih dalam file ItemDetailsScreen.kt, scroll ke fungsi ItemDetailsBody().

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:

pop-up konfirmasi penghapusan item

  1. Jalankan aplikasi.
  2. Pilih elemen daftar di layar Inventory.
  3. Di layar Item Details, ketuk Delete.
  4. Ketuk Yes pada dialog pemberitahuan, dan aplikasi akan membuka kembali layar Inventory.
  5. Pastikan entity yang dihapus tidak ada lagi di database aplikasi.

Selamat, Anda telah berhasil menerapkan fitur hapus.

Layar detail item dengan jendela dialog Pemberitahuan.

Layar inventory tanpa item yang dihapus

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

  1. Di ItemDaoTest.kt, tambahkan pengujian bernama daoGetItem_returnsItemFromDB().
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
  1. Tentukan fungsi. Di dalam coroutine, tambahkan satu item ke database.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
    addOneItemToDb()
}
  1. Ambil entity dari database menggunakan fungsi itemDao.getItem() dan tetapkan ke val bernama item.
val item = itemDao.getItem(1)
  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)
}
  1. 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.

Layar detail item dengan tombol jual diaktifkan

Layar edit item dengan kolom kosong

  1. Di ItemDetailsScreen.kt, scroll ke composable ItemDetailsScreen.
  2. Di FloatingActionButton(), ubah argumen onClick untuk menyertakan uiState.value.itemDetails.id, yang merupakan id dari entity yang dipilih. Anda dapat menggunakan id ini untuk mengambil detail entity.
FloatingActionButton(
    onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
    modifier = /*...*/
)
  1. Di class ItemEditViewModel, tambahkan blok init.
init {

}
  1. Di dalam blok init, luncurkan coroutine dengan viewModelScope.launch.
import kotlinx.coroutines.launch

viewModelScope.launch { }
  1. Di dalam blok launch, ambil detail entity dengan itemsRepository.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.

  1. Tambahkan parameter konstruktor ke class ItemEditViewModel:
class ItemEditViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
)
  1. Pada file AppViewModelProvider.kt, di penginisialisasi ItemEditViewModel, tambahkan objek ItemsRepository sebagai argumen.
initializer {
    ItemEditViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Jalankan aplikasi.
  2. Buka Item Details, lalu ketuk 2ae4a1588eba091b.png FAB.
  3. Perhatikan bahwa kolom diisi dengan detail item.
  4. 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.

Layar detail item dengan tombol jual diaktifkan

Layar edit item dengan kolom kosong

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.

  1. Di class ItemEditViewModel, tambahkan fungsi bernama updateUiState() yang menggunakan objek ItemUiState dan tidak menampilkan apa pun. Fungsi ini mengupdate itemUiState 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.

  1. Buka file ItemEditScreen.kt.
  2. Di composable ItemEditScreen, scroll ke bawah ke panggilan fungsi ItemEntryBody().
  3. Tetapkan nilai argumen onItemValueChange ke fungsi baru updateUiState.
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = modifier.padding(innerPadding)
)
  1. Jalankan aplikasi.
  2. Buka layar Edit Item.
  3. Buat salah satu nilai entity kosong sehingga tidak valid. Perhatikan cara tombol Save dinonaktifkan secara otomatis.

Layar detail item dengan tombol jual (sell) diaktifkan

Layar edit Item dengan semua kolom teks masuk dan tombol Save diaktifkan

Layar edit Item dengan tombol Save dinonaktifkan

  1. Kembali ke class ItemEditViewModel dan tambahkan fungsi suspend bernama updateItem() yang tidak memerlukan apa pun. Anda menggunakan fungsi ini untuk menyimpan entity yang telah diupdate ke database Room.
suspend fun updateItem() {
}
  1. Di dalam fungsi getUpdatedItemEntry(), tambahkan kondisi if untuk memvalidasi input pengguna dengan menggunakan fungsi validateInput().
  2. Lakukan panggilan ke fungsi updateItem() di itemsRepository, yang meneruskan itemUiState.itemDetails.toItem(). Entity yang dapat ditambahkan ke database Room harus berjenis Item. Fungsi yang sudah selesai akan terlihat seperti berikut:
suspend fun updateItem() {
    if (validateInput(itemUiState.itemDetails)) {
        itemsRepository.updateItem(itemUiState.itemDetails.toItem())
    }
}
  1. Kembali ke composable ItemEditScreen. Anda memerlukan cakupan coroutine untuk memanggil fungsi updateItem(). Buat val bernama coroutineScope dan tetapkan ke rememberCoroutineScope().
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Pada panggilan fungsi ItemEntryBody(), update argumen fungsi onSaveClick untuk memulai coroutine dalam coroutineScope.
  2. Di dalam blok launch, panggil updateItem() pada viewModel 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)
)
  1. Jalankan aplikasi dan coba edit item inventaris. Anda sekarang dapat mengedit item apa pun di database aplikasi Inventory.

Layar edit item detail item diedit

Layar detail item dengan detail item yang diperbarui

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

Referensi Kotlin