Đọc và cập nhật dữ liệu thông qua Room

1. Trước khi bắt đầu

Trong các lớp học lập trình trước, bạn đã tìm hiểu cách sử dụng thư viện dữ liệu cố định Room, một tầng trừu tượng ở đầu cơ sở dữ liệu SQLite để lưu trữ dữ liệu của ứng dụng. Trong lớp học lập trình này, bạn sẽ bổ sung thêm nhiều tính năng vào ứng dụng Inventory (Kiểm kho) và tìm hiểu cách đọc, hiển thị, cập nhật và xoá dữ liệu khỏi cơ sở dữ liệu SQLite thông qua Room. Bạn sẽ sử dụng LazyColumn để hiển thị dữ liệu từ cơ sở dữ liệu và tự động cập nhật dữ liệu khi dữ liệu cơ sở trong cơ sở dữ liệu thay đổi.

Điều kiện tiên quyết

  • Có thể tạo và tương tác với cơ sở dữ liệu SQLite bằng thư viện Room.
  • Có thể tạo thực thể, đối tượng truy cập dữ liệu và lớp cơ sở dữ liệu.
  • Có thể sử dụng đối tượng truy cập dữ liệu (DAO) để ánh xạ các hàm Kotlin đến truy vấn SQL.
  • Có thể hiển thị các mục danh sách trong LazyColumn.
  • Đã hoàn thành lớp học lập trình trước trong học phần này, Duy trì dữ liệu thông qua Room.

Kiến thức bạn sẽ học được

  • Cách đọc và hiển thị thực thể từ cơ sở dữ liệu SQLite.
  • Cách cập nhật và xoá thực thể trong cơ sở dữ liệu SQLite bằng cách sử dụng thư viện Room.

Sản phẩm bạn sẽ tạo ra

  • Ứng dụng Inventory (Kiểm kho) cho thấy danh sách mặt hàng tồn kho, đồng thời có thể sử dụng Room để cập nhật, chỉnh sửa và xoá các mặt hàng khỏi cơ sở dữ liệu ứng dụng.

Bạn cần có

  • Máy tính đã cài đặt Android Studio

2. Tổng quan về ứng dụng khởi đầu

Lớp học lập trình này sử dụng mã giải pháp của ứng dụng Inventory (Kiểm kho) trong lớp học lập trình trước (Duy trì dữ liệu thông qua Room) làm mã khởi đầu. Ứng dụng khởi đầu lưu dữ liệu bằng thư viện duy trì dữ liệu Room. Người dùng có thể sử dụng màn hình Add Item (Thêm mặt hàng) để thêm dữ liệu vào cơ sở dữ liệu ứng dụng.

Màn hình Add item (Thêm mặt hàng) có các trường văn bản trống

Màn hình điện thoại cho thấy kho hàng còn trống

Trong lớp học lập trình này, bạn mở rộng ứng dụng này để đọc và hiển thị dữ liệu, cũng như cập nhật và xoá thực thể trên cơ sở dữ liệu bằng thư viện Room.

Tải mã khởi đầu cho lớp học lập trình này

Để bắt đầu, hãy tải mã khởi đầu xuống:

$ 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

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp zip rồi giải nén và mở trong Android Studio.

Nếu bạn muốn xem mã khởi đầu cho lớp học lập trình này, hãy xem trên GitHub.

3. Cập nhật trạng thái giao diện người dùng

Trong nhiệm vụ này, bạn thêm LazyColumn vào ứng dụng để hiển thị dữ liệu lưu trữ trong cơ sở dữ liệu.

Màn hình điện thoại cho thấy các mặt hàng tồn kho

Hướng dẫn từng bước về hàm có khả năng kết hợp HomeScreen

  • Mở lại tệp ui/home/HomeScreen.kt và xem thành phần kết hợp 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()
        )
    }

Hàm có khả năng kết hợp này cho thấy các mục sau:

  • Thanh ứng dụng trên cùng có tên ứng dụng
  • Nút hành động nổi (FAB) để thêm các mặt hàng mới vào kho hàng adc468afa54b6e70.png
  • Hàm có khả năng kết hợp HomeBody()

Hàm có khả năng kết hợp HomeBody() cho thấy các mặt hàng tồn kho dựa trên danh sách đã được truyền vào. Trong quá trình triển khai mã khởi đầu, một danh sách trống (listOf()) sẽ được truyền đến hàm có khả năng kết hợp HomeBody(). Để truyền danh sách hàng tồn kho đến thành phần kết hợp này, bạn phải truy xuất dữ liệu kho hàng từ kho lưu trữ rồi truyền dữ liệu này vào HomeViewModel.

Phát ra trạng thái giao diện người dùng trong HomeViewModel

Khi thêm các phương thức vào ItemDao để lấy các mục, (getItem()getAllItems()) bạn đã chỉ định Flow làm kiểu dữ liệu trả về. Hãy nhớ rằng Flow đại diện cho một luồng dữ liệu chung. Khi trả về một Flow, bạn chỉ cần thể hiện rõ lệnh gọi các phương thức qua DAO một lần cho mỗi vòng đời nhất định. Room sẽ xử lý các nội dung cập nhật cho dữ liệu cơ bản theo cách không đồng bộ.

Việc lấy dữ liệu qua một luồng được gọi là thu thập qua một luồng. Khi thu thập qua một luồng trong lớp giao diện người dùng, bạn cần cân nhắc một số điều sau đây.

  • Các sự kiện trong vòng đời như thay đổi cấu hình (ví dụ: xoay thiết bị) sẽ khiến hoạt động được tạo lại. Điều này sẽ dẫn đến quá trình kết hợp lại và thu thập lại qua Flow của bạn hết lần này đến lần khác.
  • Bạn nên lưu các giá trị vào bộ nhớ đệm dưới dạng trạng thái để dữ liệu hiện có không bị mất khi các sự kiện trong vòng đời xảy ra.
  • Bạn nên huỷ luồng khi không còn trình quan sát nào, chẳng hạn như sau khi vòng đời của một thành phần kết hợp kết thúc.

Bạn nên hiển thị Flow qua ViewModel bằng StateFlow. Việc sử dụng StateFlow cho phép lưu và quan sát dữ liệu, bất kể vòng đời của giao diện người dùng. Bạn cần sử dụng toán tử stateIn để chuyển đổi Flow thành StateFlow.

Toán tử stateIn có 3 tham số được giải thích ở dưới đây:

  • scopeviewModelScope xác định vòng đời của StateFlow. Khi viewModelScope bị huỷ, StateFlow cũng sẽ bị huỷ.
  • started – Quy trình chỉ hoạt động khi giao diện người dùng được hiển thị. SharingStarted.WhileSubscribed() được dùng để hoàn tất việc này. Để định cấu hình độ trễ (tính bằng mili giây) từ lúc trình đăng ký gần đây nhất biến mất đến khi dừng chia sẻ coroutine, hãy truyền TIMEOUT_MILLIS vào phương thức SharingStarted.WhileSubscribed().
  • initialValue – Thiết lập giá trị ban đầu của luồng trạng thái thành HomeUiState().

Sau khi chuyển đổi Flow thành StateFlow, bạn có thể thu thập luồng này bằng phương thức collectAsState() và chuyển đổi dữ liệu của luồng này thành State có cùng kiểu.

Ở bước này, bạn truy xuất tất cả mục trong cơ sở dữ liệu Room dưới dạng API quan sát được StateFlow cho trạng thái giao diện người dùng. Khi dữ liệu Room của ứng dụng Inventory (Kiểm kho) thay đổi, giao diện người dùng sẽ tự động cập nhật.

  1. Mở tệp ui/home/HomeViewModel.kt, chứa hằng số TIMEOUT_MILLIS và lớp dữ liệu HomeUiState với danh sách các mặt hàng đóng vai trò tham số hàm khởi tạo.
// 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. Bên trong lớp HomeViewModel, hãy khai báo val có tên là homeUiState thuộc kiểu StateFlow<HomeUiState>. Bạn sẽ sớm khắc phục được lỗi khởi động.
val homeUiState: StateFlow<HomeUiState>
  1. Gọi getAllItemsStream() trên itemsRepository rồi gán cho homeUiState mà bạn vừa khai báo.
val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream()

Bây giờ bạn gặp lỗi – Unresolved reference: itemsRepository (Tham chiếu chưa được giải quyết: itemsRepository). Để giải quyết lỗi Tham chiếu chưa được giải quyết, bạn cần truyền đối tượng ItemsRepository vào HomeViewModel.

  1. Thêm một tham số hàm khởi tạo thuộc kiểu ItemsRepository vào lớp HomeViewModel.
import com.example.inventory.data.ItemsRepository

class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
  1. Ở tệp ui/AppViewModelProvider.kt, trong trình khởi tạo của HomeViewModel, hãy truyền đối tượng ItemsRepository như minh hoạ.
initializer {
    HomeViewModel(inventoryApplication().container.itemsRepository)
}
  1. Quay lại tệp HomeViewModel.kt. Hãy lưu ý đến lỗi kiểu dữ liệu không khớp (type mismatch). Để giải quyết vấn đề này, hãy thêm bản đồ chuyển đổi như minh hoạ dưới đây.
val homeUiState: StateFlow<HomeUiState> =
    itemsRepository.getAllItemsStream().map { HomeUiState(it) }

Android Studio vẫn hiện lỗi kiểu dữ liệu không khớp (type mismatch) Lỗi này là do homeUiState thuộc kiểu StateFlowgetAllItemsStream() trả về Flow.

  1. Sử dụng toán tử stateIn để chuyển đổi Flow thành StateFlow. StateFlow là API có thể quan sát dành cho trạng thái giao diện người dùng, cho phép giao diện người dùng tự cập nhật.
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. Tạo bản dựng ứng dụng để đảm bảo không có lỗi trong mã. Bạn sẽ không thấy xuất hiện bất cứ thay đổi nào.

4. Hiển thị dữ liệu Kho hàng

Trong nhiệm vụ này, bạn thu thập và cập nhật trạng thái giao diện người dùng trong HomeScreen.

  1. Ở tệp HomeScreen.kt, trong hàm có khả năng kết hợp HomeScreen, hãy thêm một tham số hàm mới thuộc kiểu HomeViewModel và khởi tạo tham số đó.
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. Trong hàm có khả năng kết hợp HomeScreen, hãy thêm một val có tên là homeUiState để thu thập trạng thái giao diện người dùng qua HomeViewModel. Bạn sử dụng collectAsState() để thu thập các giá trị qua StateFlow này và thể hiện giá trị mới nhất của hàm này qua State.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue

val homeUiState by viewModel.homeUiState.collectAsState()
  1. Cập nhật lệnh gọi hàm HomeBody() rồi truyền homeUiState.itemList vào tham số itemList.
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier.padding(innerPadding)
)
  1. Chạy ứng dụng. Lưu ý rằng danh sách hàng tồn kho sẽ xuất hiện nếu bạn đã lưu các mặt hàng trong cơ sở dữ liệu ứng dụng. Nếu danh sách này còn trống, hãy thêm một số mặt hàng tồn kho vào cơ sở dữ liệu ứng dụng.

Màn hình điện thoại cho thấy các mặt hàng tồn kho

5. Kiểm thử cơ sở dữ liệu

Chúng ta đã thảo luận về tầm quan trọng của việc kiểm thử đoạn mã trong các lớp học lập trình trước. Trong nhiệm vụ này, bạn thêm một số chương trình kiểm thử đơn vị để kiểm thử các truy vấn DAO, rồi thêm nhiều chương trình kiểm thử hơn trong quá trình tham gia lớp học lập trình này.

Bạn nên sử dụng phương pháp viết chương trình kiểm thử JUnit chạy trên thiết bị Android để kiểm thử việc triển khai cơ sở dữ liệu của mình. Vì các chương trình kiểm thử này không yêu cầu tạo hoạt động nên sẽ có tốc độ thực thi nhanh hơn so với chương trình kiểm thử giao diện người dùng.

  1. Trong tệp build.gradle.kts (Module :app), hãy lưu ý các phần phụ thuộc sau cho Espresso và JUnit.
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
  1. Chuyển sang chế độ xem Project (Dự án) rồi nhấp chuột phải vào src > New (Mới) > Directory (Thư mục) để tạo nhóm tài nguyên kiểm thử cho chương trình kiểm thử.

e53b0f0e0b6aba29.png

  1. Chọn androidTest/kotlin trong cửa sổ bật lên New Directory (Thư mục mới).

860b7e1af5f116a.png

  1. Tạo một lớp Kotlin tên là ItemDaoTest.kt.
  2. Chú giải lớp ItemDaoTest này bằng @RunWith(AndroidJUnit4::class). Hiện tại, lớp (class) của bạn có dạng như mã ví dụ sau:
package com.example.inventory

import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
  1. Bên trong lớp này, hãy thêm các biến var riêng tư thuộc kiểu ItemDaoInventoryDatabase.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao

private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
  1. Thêm một hàm để tạo cơ sở dữ liệu và chú thích cơ sở dữ liệu đó bằng @Before để có thể chạy trước mỗi lần kiểm thử.
  2. Bên trong phương thức này, hãy khởi tạo 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()
}

Trong hàm này, bạn sử dụng một cơ sở dữ liệu trong bộ nhớ và đừng lưu trữ cơ sở dữ liệu này trên ổ đĩa. Để thực hiện việc này, bạn hãy sử dụng hàm inMemoryDatabaseBuilder(). Thực hiện việc này là vì bạn không cần phải duy trì thông tin, mà cần xoá khi quá trình bị dừng. Bạn đang chạy các truy vấn DAO trong luồng chính bằng .allowMainThreadQueries() chỉ để kiểm thử.

  1. Thêm một hàm khác để đóng cơ sở dữ liệu. Chú giải hàm này bằng @After để đóng cơ sở dữ liệu rồi chạy sau mỗi lần kiểm thử.
import org.junit.After
import java.io.IOException

@After
@Throws(IOException::class)
fun closeDb() {
    inventoryDatabase.close()
}
  1. Khai báo các mặt hàng trong lớp ItemDaoTest để cơ sở dữ liệu sử dụng, như trong đoạn mã ví dụ sau:
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. Thêm các hàm số hiệu dụng để thêm một mặt hàng, rồi hai mặt hàng vào cơ sở dữ liệu. Sau đó, bạn sử dụng các hàm này trong chương trình kiểm thử. Hãy đánh dấu các hàm này là suspend để chúng có thể chạy trong coroutine.
private suspend fun addOneItemToDb() {
    itemDao.insert(item1)
}

private suspend fun addTwoItemsToDb() {
    itemDao.insert(item1)
    itemDao.insert(item2)
}
  1. Viết chương trình kiểm thử để chèn một mặt hàng riêng biệt vào cơ sở dữ liệu, insert(). Đặt tên cho chương trình kiểm thử là daoInsert_insertsItemIntoDB và chú thích bằng @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)
}

Trong chương trình kiểm thử này, bạn sử dụng hàm hiệu dụng addOneItemToDb() để thêm một mặt hàng vào cơ sở dữ liệu. Sau đó, bạn đọc mặt hàng đầu tiên trong cơ sở dữ liệu. Với assertEquals(), bạn so sánh giá trị dự kiến với giá trị thực tế. Bạn chạy chương trình kiểm thử trong một coroutine mới bằng runBlocking{}. Do cách thiết lập này, bạn đánh dấu các hàm hiệu dụng là suspend.

  1. Chạy chương trình kiểm thử và đảm bảo có được kết quả kiểm thử đạt.

cd95648114520f13.png

6521e8595bb33a91.png

  1. Viết một chương trình kiểm thử khác cho getAllItems() qua cơ sở dữ liệu. Đặt tên cho chương trình kiểm thử đó là daoGetAllItems_returnsAllItemsFromDB.
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
    addTwoItemsToDb()
    val allItems = itemDao.getAllItems().first()
    assertEquals(allItems[0], item1)
    assertEquals(allItems[1], item2)
}

Trong chương trình kiểm thử trên, bạn thêm hai mặt hàng vào cơ sở dữ liệu bên trong coroutine. Sau đó, bạn đọc hai nội dung và so sánh với các giá trị dự kiến.

6. Hiện thông tin về mặt hàng

Trong nhiệm vụ này, bạn đọc và hiển thị thông tin về thực thể trên màn hình Item Details (Thông tin về mặt hàng). Bạn sử dụng trạng thái của giao diện người dùng về mặt hàng (ví dụ: tên, giá và số lượng) trong cơ sở dữ liệu của ứng dụng Inventory (Kiểm kho) rồi hiện những thông tin này trên màn hình Item Details (Thông tin chi tiết về mặt hàng) bằng thành phần kết hợp ItemDetailsScreen. Hàm có khả năng kết hợp ItemDetailsScreen được viết sẵn cho bạn và chứa 3 thành phần kết hợp Văn bản cho thấy thông tin chi tiết về mặt hàng.

ui/item/ItemDetailsScreen.kt

Màn hình này là một phần của mã khởi đầu và thể hiện thông tin về các mặt hàng mà bạn sẽ thấy trong một lớp học lập trình sau. Trong lớp học lập trình này, bạn chưa làm việc trên màn hình này. ItemDetailsViewModel.ktViewModel tương ứng cho màn hình này.

a5009ad021b830ff.png

  1. Trong hàm có khả năng kết hợp HomeScreen, hãy lưu ý lệnh gọi hàm HomeBody(). navigateToItemUpdate đang được truyền đến tham số onItemClick. Tham số này sẽ được gọi khi bạn nhấp vào một mặt hàng nào đó trong danh sách.
// No need to copy over
HomeBody(
    itemList = homeUiState.itemList,
    onItemClick = navigateToItemUpdate,
    modifier = modifier
        .padding(innerPadding)
        .fillMaxSize()
)
  1. Mở ui/navigation/InventoryNavGraph.kt và chú ý đến tham số navigateToItemUpdate trong thành phần kết hợp HomeScreen. Tham số này chỉ định màn hình thông tin chi tiết về mặt hàng làm đích đến cho hoạt động điều hướng.
// No need to copy over
HomeScreen(
    navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
    navigateToItemUpdate = {
        navController.navigate("${ItemDetailsDestination.route}/${it}")
   }

Phần này của chức năng onItemClick đã được triển khai cho bạn. Khi bạn nhấp vào mặt hàng trong danh sách, ứng dụng sẽ chuyển đến màn hình chi tiết của mặt hàng đó.

  1. Nhấp vào mặt hàng trong danh sách hàng tồn kho để xem màn hình thông tin về mặt hàng có các trường trống.

Màn hình Item details (Thông tin về mặt hàng) có các trường trống

Để điền thông tin về mặt hàng vào các trường văn bản, bạn cần thu thập trạng thái giao diện người dùng trong ItemDetailsScreen().

  1. Trong UI/Item/ItemDetailsScreen.kt, hãy thêm một tham số mới vào thành phần kết hợp ItemDetailsScreen thuộc kiểu ItemDetailsViewModel và sử dụng phương thức ban đầu để khởi tạo tham số đó.
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. Bên trong thành phần kết hợp ItemDetailsScreen(), hãy tạo một val có tên là uiState để thu thập trạng thái giao diện người dùng. Sử dụng collectAsState() để thu thập StateFlow uiState rồi biểu thị giá trị mới nhất của nó qua State. Android Studio sẽ hiện lỗi tham chiếu chưa được giải quyết (unresolved reference).
import androidx.compose.runtime.collectAsState

val uiState = viewModel.uiState.collectAsState()
  1. Để khắc phục lỗi này, hãy tạo một val có tên là uiState thuộc loại StateFlow<ItemDetailsUiState> trong lớp ItemDetailsViewModel.
  2. Truy xuất dữ liệu qua kho lưu trữ mặt hàng rồi ánh xạ dữ liệu đó vào ItemDetailsUiState bằng hàm mở rộng toItemDetails(). Hàm mở rộng Item.toItemDetails() được viết sẵn cho bạn trong mã khởi đầu.
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. Truyền ItemsRepository vào ItemDetailsViewModel để giải quyết lỗi Unresolved reference: itemsRepository.
class ItemDetailsViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
    ) : ViewModel() {
  1. Trong ui/AppViewModelProvider.kt, hãy cập nhật trình khởi tạo cho ItemDetailsViewModel như trong đoạn mã sau:
initializer {
    ItemDetailsViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Quay lại ItemDetailsScreen.kt, bạn nhận thấy lỗi trong thành phần kết hợp ItemDetailsScreen() đã được giải quyết.
  2. Trong thành phần kết hợp ItemDetailsScreen(), hãy cập nhật lệnh gọi hàm ItemDetailsBody() và truyền uiState.value vào đối số itemUiState.
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = {  },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Quan sát quá trình triển khai ItemDetailsBody()ItemInputForm(). Bạn đang truyền item được chọn hiện tại từ ItemDetailsBody() đến 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. Chạy ứng dụng. Khi bạn nhấp vào một phần tử danh sách trên màn hình Inventory (Kho hàng), màn hình Item Details (Thông tin về mặt hàng) sẽ xuất hiện.
  2. Lưu ý rằng màn hình còn không trống nữa. Màn hình cho biết thông tin về thực thể được truy xuất qua cơ sở dữ liệu kho hàng.

Màn hình Item details (Thông tin về mặt hàng) cho thấy thông tin về mặt hàng được điền sẵn

  1. Nhấn vào nút Sell (Bán). Không có gì xảy ra cả!

Trong phần tiếp theo, bạn triển khai chức năng của nút Sell (Bán).

7. Triển khai màn hình Item details (Thông tin về mặt hàng)

ui/item/ItemEditScreen.kt

Màn hình Item edit (Chỉnh sửa mặt hàng) được cung cấp cho bạn trong mã khởi đầu.

Bố cục này chứa các thành phần kết hợp trường văn bản để chỉnh sửa chi tiết của mọi mặt hàng mới trong kho hàng.

Màn hình Edit item (Chỉnh sửa mặt hàng) có các đối tượng trống

Ứng dụng này có mã chưa đầy đủ chức năng. Ví dụ: trong màn hình Item Details (Thông tin chi tiết về mặt hàng), khi bạn nhấn vào nút Sell (Bán), Quantity in Stock (Số lượng hàng tồn kho) sẽ không giảm. Khi bạn nhấn vào nút Delete (Xoá), ứng dụng sẽ hiện một hộp thoại xác nhận để nhắc bạn. Tuy nhiên, khi bạn chọn nút Yes (Có), ứng dụng sẽ không thực sự xoá mặt hàng đó.

cửa sổ bật lên xác nhận việc xoá mặt hàng

Cuối cùng, nút hành động nổi be6c7ed4ac207351.png sẽ mở màn hình Edit Item (Chỉnh sửa mặt hàng) trống.

Màn hình Edit item (Chỉnh sửa mặt hàng) có các đối tượng trống

Trong phần này, bạn triển khai các chức năng của nút Sell (Bán), Delete (Xoá) và các nút hành động nổi.

8. Triển khai chức năng bán mặt hàng

Trong phần này, bạn mở rộng các tính năng của ứng dụng để triển khai chức năng bán. Bước cập nhật này bao gồm những nhiệm vụ sau:

  • Thêm chương trình kiểm thử cho hàm DAO để cập nhật một thực thể.
  • Thêm một hàm trong ItemDetailsViewModel để giảm số lượng mặt hàng và cập nhật thực thể tương ứng trong cơ sở dữ liệu ứng dụng.
  • Tắt nút Sell (Bán) nếu số lượng là 0.
  1. Trong ItemDaoTest.kt, hãy thêm một hàm tên là daoUpdateItems_updatesItemsInDB(); hàm này không có tham số. Chú giải bằng @Test@Throws(Exception::class).
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
  1. Định nghĩa hàm này và tạo một khối runBlocking. Hãy gọi addTwoItemsToDb() bên trong đó.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
    addTwoItemsToDb()
}
  1. Cập nhật 2 thực thể này với các giá trị khác nhau, gọi itemDao.update.
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
  1. Truy xuất các thực thể bằng itemDao.getAllItems(). So sánh các thực thể đó với thực thể đã cập nhật và xác nhận.
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
  1. Hãy đảm bảo rằng hàm hoàn chỉnh có dạng như sau:
@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. Chạy chương trình kiểm thử và đảm bảo có được kết quả kiểm thử đạt.

Thêm một hàm trong ViewModel

  1. Trong ItemDetailsViewModel.kt, bên trong lớp ItemDetailsViewModel, hãy thêm một hàm có tên là reduceQuantityByOne(); hàm này không có tham số.
fun reduceQuantityByOne() {
}
  1. Bên trong hàm này, hãy khởi động một coroutine bằng viewModelScope.launch{}.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope

viewModelScope.launch {
}
  1. Bên trong khối launch, hãy tạo một val có tên là currentItem rồi thiết lập thành uiState.value.toItem().
val currentItem = uiState.value.toItem()

uiState.value thuộc kiểu ItemUiState. Bạn chuyển đổi nó thành kiểu thực thể Item có hàm mở rộng toItem().

  1. Thêm câu lệnh if để kiểm tra xem quality có lớn hơn 0 hay không.
  2. Gọi updateItem() trên itemsRepository rồi truyền currentItem đã cập nhật vào. Cập nhật giá trị quantity bằng copy() để hàm này có dạng như sau:
fun reduceQuantityByOne() {
    viewModelScope.launch {
        val currentItem = uiState.value.itemDetails.toItem()
        if (currentItem.quantity > 0) {
    itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
       }
    }
}
  1. Quay lại ItemDetailsScreen.kt.
  2. Trong thành phần kết hợp ItemDetailsScreen, hãy chuyển đến lệnh gọi hàm ItemDetailsBody().
  3. Trong hàm lambda onSellItem, hãy gọi viewModel.reduceQuantityByOne().
ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = { },
    modifier = modifier.padding(innerPadding)
)
  1. Chạy ứng dụng.
  2. Trên màn hình Inventory (Kho hàng), hãy nhấp vào một phần tử trong danh sách. Khi màn hình Item Details (Thông tin về mặt hàng) xuất hiện, hãy nhấn vào nút Sell (Bán) để thấy rằng giá trị của số lượng mặt hàng giảm đi 1.

Màn hình Item details (Thông tin về mặt hàng) giảm số lượng đi 1 khi nhấn vào nút sell (bán)

  1. Trên màn hình Item Details (Thông tin về mặt hàng), hãy liên tục nhấn vào nút Sell (Bán) cho đến khi số lượng trở thành 0.

Sau khi số lượng trở về 0, hãy nhấn vào nút Sell (Bán) một lần nữa. Bạn không nhìn thấy thay đổi nào vì hàm reduceQuantityByOne() sẽ kiểm tra xem số lượng có lớn hơn 0 hay không trước khi cập nhật số lượng.

Màn hình Item details (Thông tin về mặt hàng) có số lượng là 0

Để cung cấp cho người dùng phản hồi tốt hơn, bạn nên vô hiệu hoá nút Sell (Bán) khi không có mặt hàng để bán.

  1. Trong lớp ItemDetailsViewModel, hãy đặt giá trị outOfStock dựa trên it.quantity trong phép biến đổi map.
val uiState: StateFlow<ItemDetailsUiState> =
    itemsRepository.getItemStream(itemId)
        .filterNotNull()
        .map {
            ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
        }.stateIn(
            //...
        )
  1. Chạy ứng dụng của bạn. Lưu ý rằng ứng dụng sẽ tắt nút Sell (Bán) khi số lượng hàng tồn kho bằng 0.

Màn hình Item details (Thông tin về mặt hàng) có nút Sell (Bán) đang tắt

Chúc mừng bạn đã triển khai thành công tính năng Sell (Bán) mặt hàng cho ứng dụng của mình.

Xoá thực thể mặt hàng

Cũng giống như nhiệm vụ trước, bạn phải mở rộng tính năng của ứng dụng hơn nữa bằng cách triển khai tính năng xoá. Tính năng này dễ triển khai hơn nhiều so với tính năng bán. Quá trình này bao gồm những việc sau:

  • Thêm chương trình kiểm thử cho truy vấn xoá DAO.
  • Thêm một hàm trong ItemDetailsViewModel để xoá một thực thể khỏi cơ sở dữ liệu
  • Cập nhật thành phần kết hợp ItemDetailsBody.

Thêm chương trình kiểm thử DAO

  1. Trong ItemDaoTest.kt, hãy thêm một chương trình kiểm thử có tên daoDeleteItems_deletesAllItemsFromDB().
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
  1. Chạy một coroutine bằng runBlocking {}.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
  1. Thêm hai mặt hàng vào cơ sở dữ liệu và gọi itemDao.delete() trên hai mặt hàng đó để xoá khỏi cơ sở dữ liệu.
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
  1. Truy xuất các thực thể qua cơ sở dữ liệu và kiểm tra để đảm bảo rằng danh sách này trống. Chương trình kiểm thử hoàn chỉnh sẽ có dạng như sau:
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())
}

Thêm hàm xoá trong ItemDetailsViewModel

  1. Trong ItemDetailsViewModel, hãy thêm một hàm mới có tên là deleteItem(). Hàm này không chứa tham số và không trả về giá trị nào.
  2. Bên trong hàm deleteItem(), hãy thêm một lệnh gọi hàm itemsRepository.deleteItem() và truyền vào uiState.value.toItem().
suspend fun deleteItem() {
    itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}

Trong hàm này, bạn chuyển đổi uiState từ kiểu itemDetails thành kiểu thực thể Item bằng cách sử dụng hàm mở rộng toItem().

  1. Trong thành phần kết hợp ui/item/ItemDetailsScreen, hãy thêm một val có tên là coroutineScope rồi thiết lập thành rememberCoroutineScope(). Phương pháp này trả về một phạm vi coroutine liên kết với quá trình tổng hợp mà nó được gọi (thành phần kết hợp ItemDetailsScreen).
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Di chuyển đến hàm ItemDetailsBody().
  2. Chạy một coroutine với coroutineScope là bên trong hàm lambda onDelete.
  3. Trong khối launch, hãy gọi phương thức deleteItem() trên viewModel.
import kotlinx.coroutines.launch

ItemDetailsBody(
    itemUiState = uiState.value,
    onSellItem = { viewModel.reduceQuantityByOne() },
    onDelete = {
        coroutineScope.launch {
           viewModel.deleteItem()
    }
    modifier = modifier.padding(innerPadding)
)
  1. Sau khi xoá mặt hàng, hãy quay lại màn hình thông tin kho hàng.
  2. Gọi navigateBack() sau lệnh gọi hàm deleteItem().
onDelete = {
    coroutineScope.launch {
        viewModel.deleteItem()
        navigateBack()
    }
  1. Vẫn trong tệp ItemDetailsScreen.kt, hãy di chuyển đến hàm ItemDetailsBody().

Mã khởi đầu cũng cung cấp hàm này cho bạn. Thành phần kết hợp này cho thấy hộp thoại cảnh báo để lấy xác nhận của người dùng trước khi xoá mặt hàng và gọi hàm deleteItem() khi bạn nhấn vào Yes (Có).

// 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()
                },
                //...
            )
        }
    }
}

Khi bạn nhấn vào No (Không), ứng dụng sẽ đóng hộp thoại cảnh báo. Hàm showConfirmationDialog() sẽ đưa ra cảnh báo sau:

cửa sổ bật lên xác nhận việc xoá mặt hàng

  1. Chạy ứng dụng.
  2. Chọn một phần tử trong danh sách trên màn hình Inventory (Kiểm kho).
  3. Trên màn hình Item Details (Thông tin chi tiết về mặt hàng), hãy nhấn vào Delete (Xoá).
  4. Nhấn vào Yes (Có) trong hộp thoại cảnh báo, và ứng dụng sẽ quay lại màn hình Inventory (Kiểm kho).
  5. Lưu ý rằng thực thể bạn đã xoá không còn nằm trong cơ sở dữ liệu của ứng dụng.

Chúc mừng bạn đã triển khai thành công tính năng xoá!

Màn hình Item details (Thông tin về mặt hàng) chứa cửa sổ Alert (Cảnh báo).

Màn hình Inventory (Kho hàng) không có mặt hàng đã xoá

Chỉnh sửa thực thể mặt hàng

Tương tự như các phần trước, trong phần này, bạn thêm một tính năng nâng cao khác cho ứng dụng để chỉnh sửa thực thể của mặt hàng.

Sau đây là hướng dẫn ngắn gọn về các bước để chỉnh sửa thực thể trong cơ sở dữ liệu ứng dụng:

  • Thêm một chương trình kiểm thử vào truy vấn để lấy DAO của mặt hàng.
  • Điền thông tin về thực thể vào các trường văn bản và màn hình Edit Item (Chỉnh sửa mặt hàng).
  • Cập nhật thực thể trong cơ sở dữ liệu bằng cách sử dụng Room.

Thêm chương trình kiểm thử DAO

  1. Trong ItemDaoTest.kt, hãy thêm một chương trình kiểm thử có tên daoGetItem_returnsItemFromDB().
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
  1. Định nghĩa hàm này. Bên trong coroutine, hãy thêm một mặt hàng vào cơ sở dữ liệu.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
    addOneItemToDb()
}
  1. Truy xuất thực thể qua cơ sở dữ liệu bằng cách sử dụng hàm itemDao.getItem() và thiết lập thực thể đó thành val có tên là item.
val item = itemDao.getItem(1)
  1. So sánh giá trị thực tế với giá trị được truy xuất và xác nhận bằng assertEquals(). Chương trình kiểm thử đã hoàn tất của bạn có dạng như sau:
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
    addOneItemToDb()
    val item = itemDao.getItem(1)
    assertEquals(item.first(), item1)
}
  1. Chạy chương trình kiểm thử và đảm bảo có được kết quả kiểm thử đạt.

Điền các trường văn bản

Nếu bạn chạy ứng dụng, hãy chuyển đến màn hình Item Details (Thông tin chi tiết về mặt hàng), sau đó nhấp vào nút hành động nổi. Bạn có thể nhận thấy rằng tiêu đề màn hình nay là Edit Item (Chỉnh sửa mặt hàng). Tuy nhiên, tất cả trường văn bản đều trống. Trong bước này, bạn điền thông tin về thực thể vào các trường văn bản trong màn hình Edit Item (Chỉnh sửa mặt hàng).

Màn hình Item details (Thông tin về mặt hàng) có nút Sell (Bán) đang tắt

Màn hình Edit item (Chỉnh sửa mặt hàng) có các trường trống

  1. Trong ItemDetailsScreen.kt, hãy di chuyển đến thành phần kết hợp ItemDetailsScreen.
  2. Trong FloatingActionButton(), hãy thay đổi đối số onClick để đưa uiState.value.itemDetails.id (là id của thực thể đã chọn) vào. Bạn sử dụng id này để truy xuất thông tin về thực thể.
FloatingActionButton(
    onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
    modifier = /*...*/
)
  1. Trong lớp ItemEditViewModel, hãy thêm một khối init.
init {

}
  1. Bên trong khối init, hãy khởi chạy một coroutine bằng viewModelScope.launch.
import kotlinx.coroutines.launch

viewModelScope.launch { }
  1. Bên trong khối launch, hãy truy xuất thông tin về thực thể bằng 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)
    }
}

Trong khối khởi chạy này, bạn có thể thêm một bộ lọc để trả về một luồng chỉ chứa các giá trị khác rỗng. Với toItemUiState(), bạn chuyển đổi thực thể item thành ItemUiState. Bạn truyền giá trị actionEnabled dưới dạng true để bật nút Save (Lưu).

Để giải quyết lỗi Unresolved reference: itemsRepository, bạn cần truyền ItemsRepository dưới dạng phần phụ thuộc vào view model.

  1. Hãy thêm một tham số hàm khởi tạo vào lớp ItemEditViewModel.
class ItemEditViewModel(
    savedStateHandle: SavedStateHandle,
    private val itemsRepository: ItemsRepository
)
  1. Ở tệp AppViewModelProvider.kt, trong trình khởi tạo ItemEditViewModel, hãy thêm đối tượng ItemsRepository làm đối số.
initializer {
    ItemEditViewModel(
        this.createSavedStateHandle(),
        inventoryApplication().container.itemsRepository
    )
}
  1. Chạy ứng dụng.
  2. Chuyển đến phần Item Details (Thông tin về mặt hàng) rồi nhấn vào nút hành động nổi 2ae4a1588eba091b.png.
  3. Hãy lưu ý rằng các trường sẽ được điền bằng thông tin chi tiết về mặt hàng.
  4. Chỉnh sửa số lượng hàng tồn kho, hoặc các trường khác, rồi nhấn vào nút Save (Lưu).

Không có gì xảy ra cả! Điều này là do bạn không cập nhật thực thể trong cơ sở dữ liệu của ứng dụng. Bạn khắc phục được vấn đề này trong phần tiếp theo.

Màn hình Item details (Thông tin về mặt hàng) có nút Sell (Bán) đang tắt

Màn hình Edit item (Chỉnh sửa mặt hàng) có trường trống

Sử dụng Room để cập nhật thực thể

Trong nhiệm vụ cuối cùng này, bạn thêm các đoạn mã cuối cùng để triển khai chức năng cập nhật. Bạn xác định các hàm cần thiết trong ViewModel rồi sử dụng các hàm đó trong ItemEditScreen.

Lại đến lúc lập trình rồi!

  1. Trong lớp ItemEditViewModel, hãy thêm một hàm tên là updateUiState(). Hàm này sẽ chứa đối tượng ItemUiState và không trả về giá trị nào. Hàm này cập nhật các giá trị mới mà người dùng nhập cho itemUiState.
fun updateUiState(itemDetails: ItemDetails) {
    itemUiState =
        ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}

Trong hàm này, bạn chỉ định itemDetails đã truyền vào cho itemUiState và cập nhật giá trị isEntryValid. Ứng dụng này sẽ bật nút Save (Lưu) nếu itemDetailstrue. Bạn chỉ thiết lập giá trị này thành true nếu giá trị đầu vào mà người dùng nhập là hợp lệ.

  1. Chuyển đến tệp ItemEditScreen.kt.
  2. Trong thành phần kết hợp ItemEditScreen, hãy di chuyển xuống lệnh gọi hàm ItemEntryBody().
  3. Thiết lập giá trị đối số onItemValueChange cho hàm mới updateUiState.
ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = { },
    modifier = modifier.padding(innerPadding)
)
  1. Chạy ứng dụng.
  2. Chuyển đến màn hình Edit Item (Chỉnh sửa mặt hàng).
  3. Làm trống một trong các giá trị của thực thể để giá trị đó không hợp lệ. Hãy lưu ý cách nút Save (Lưu) tự động tắt.

Màn hình Item details (Thông tin về mặt hàng) có nút Sell (Bán) đang bật

Màn hình Edit Item (Chỉnh sửa mặt hàng) có mọi trường văn bản và nút lưu đang bật

Màn hình Edit item (Chỉnh sửa mặt hàng) chứa nút Save (Lưu) đang tắt

  1. Quay lại lớp ItemEditViewModel và thêm một hàm suspend có tên là updateItem(). Hàm này không làm gì cả. Bạn sử dụng hàm này để lưu thực thể cập nhật vào cơ sở dữ liệu Room.
suspend fun updateItem() {
}
  1. Bên trong hàm getUpdatedItemEntry() này, hãy thêm điều kiện if để xác thực hoạt động đầu vào của người dùng bằng hàm validateInput().
  2. Gọi đến hàm updateItem() trên itemsRepository, truyền itemUiState.itemDetails.toItem() vào. Các thực thể có thể thêm vào cơ sở dữ liệu Room cần phải thuộc kiểu Item. Khi hoàn chỉnh hàm sẽ có dạng như sau:
suspend fun updateItem() {
    if (validateInput(itemUiState.itemDetails)) {
        itemsRepository.updateItem(itemUiState.itemDetails.toItem())
    }
}
  1. Quay lại thành phần kết hợp ItemEditScreen, bạn cần phạm vi coroutine để gọi hàm updateItem(). Tạo một val có tên là coroutineScope rồi thiết lập thành rememberCoroutineScope().
import androidx.compose.runtime.rememberCoroutineScope

val coroutineScope = rememberCoroutineScope()
  1. Trong lệnh gọi hàm ItemEntryBody(), hãy cập nhật đối số hàm onSaveClick để bắt đầu một coroutine trong coroutineScope.
  2. Trong khối launch, hãy gọi updateItem() trên viewModel rồi điều hướng quay lại.
import kotlinx.coroutines.launch

onSaveClick = {
    coroutineScope.launch {
        viewModel.updateItem()
        navigateBack()
    }
},

Lệnh gọi hàm ItemEntryBody() khi hoàn tất sẽ có dạng như sau:

ItemEntryBody(
    itemUiState = viewModel.itemUiState,
    onItemValueChange = viewModel::updateUiState,
    onSaveClick = {
        coroutineScope.launch {
            viewModel.updateItem()
            navigateBack()
        }
    },
    modifier = modifier.padding(innerPadding)
)
  1. Chạy ứng dụng rồi thử chỉnh sửa các mặt hàng tồn kho. Giờ đây, bạn có thể chỉnh sửa mọi mặt hàng trong cơ sở dữ liệu của ứng dụng Inventory (Kiểm kho).

Chỉnh sửa thông tin về mặt hàng trên màn hình Edit item (Chỉnh sửa mặt hàng)

Màn hình Item details (Thông tin về mặt hàng) cho thấy thông tin được cập nhật về mặt hàng

Chúc mừng! Bạn đã tạo xong ứng dụng đầu tiên sử dụng Room để quản lý cơ sở dữ liệu!

9. Mã giải pháp

Mã giải pháp cho lớp học lập trình này nằm trong kho lưu trữ và nhánh GitHub dưới đây:

10. Tìm hiểu thêm

Tài liệu dành cho nhà phát triển Android

Tài liệu tham khảo về Kotlin