1. 시작하기 전에
이전 Codelab에서는 SQLite 데이터베이스 위의 추상화 레이어인 Room 지속성 라이브러리를 사용하여 앱 데이터를 저장하는 방법을 알아봤습니다. 이 Codelab에서는 Inventory 앱에 기능을 더 추가하고 Room을 사용하여 SQLite 데이터베이스의 데이터를 읽고 표시하고 업데이트하며 삭제하는 방법을 알아봅니다. LazyColumn을 사용하여 데이터베이스의 데이터를 표시하고 데이터베이스의 기본 데이터가 변경될 때 자동으로 데이터를 업데이트합니다.
기본 요건
- Room 라이브러리를 사용하여 SQLite 데이터베이스를 만들고 상호작용하는 능력
- 항목, DAO, 데이터베이스 클래스를 만드는 능력
- 데이터 액세스 객체(DAO)를 사용하여 Kotlin 함수를 SQL 쿼리에 매핑하는 능력
LazyColumn에 목록 항목을 표시하는 능력- 이 단원의 이전 Codelab인 Room을 사용하여 데이터 유지 완료
학습할 내용
- SQLite 데이터베이스의 항목을 읽고 표시하는 방법
- Room 라이브러리를 사용하여 SQLite 데이터베이스의 항목을 업데이트하고 삭제하는 방법
빌드할 항목
- 인벤토리 항목 목록을 표시하고 Room을 사용하여 앱 데이터베이스의 항목을 업데이트하고 수정하고 삭제할 수 있는 Inventory 앱
필요한 항목
- Android 스튜디오가 설치된 컴퓨터
2. 시작 앱 개요
이 Codelab에서는 이전 Codelab인 Room을 사용하여 데이터 유지의 Inventory 앱 솔루션 코드를 시작 코드로 사용합니다. 시작 앱은 이미 Room 지속성 라이브러리를 사용하여 데이터를 저장합니다. 사용자는 Add Item 화면을 사용하여 데이터를 앱 데이터베이스에 추가할 수 있습니다.
|
|
이 Codelab에서는 앱을 확장하여 데이터를 읽고 표시하며 Room 라이브러리를 사용하여 데이터베이스의 항목을 업데이트하고 삭제합니다.
이 Codelab의 시작 코드 다운로드
시작하려면 시작 코드를 다운로드하세요.
$ 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
또는 ZIP 파일로 저장소를 다운로드한 다음 압축을 풀고 Android 스튜디오에서 열어도 됩니다.
이 Codelab의 시작 코드는 GitHub에서 확인하세요.
3. UI 상태 업데이트
이 작업에서는 LazyColumn을 앱에 추가하여 데이터베이스에 저장된 데이터를 표시합니다.

구성 가능한 HomeScreen 함수 둘러보기
ui/home/HomeScreen.kt파일을 열고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()
)
}
이 구성 가능한 함수는 다음 항목을 표시합니다.
- 앱 제목이 있는 상단 앱 바
- 인벤토리에 새 항목을 추가하기 위한 플로팅 작업 버튼(FAB)

- 구성 가능한
HomeBody()함수
구성 가능한 HomeBody() 함수는 전달된 목록을 기반으로 인벤토리 항목을 표시합니다. 시작 코드 구현의 일부로 빈 목록(listOf())이 구성 가능한 HomeBody() 함수에 전달됩니다. 이 컴포저블에 인벤토리 목록을 전달하려면 저장소에서 인벤토리 데이터를 가져와 HomeViewModel에 전달해야 합니다.
HomeViewModel에서 UI 상태 내보내기
항목을 가져오기 위해 ItemDao에 메서드(getItem() 및 getAllItems())를 추가할 때 반환 유형으로 Flow를 지정했습니다. Flow는 일반 데이터 스트림을 나타냅니다. Flow를 반환하면 지정된 수명 주기 동안 DAO에서 메서드를 한 번만 명시적으로 호출하면 됩니다. Room은 기본 데이터의 업데이트를 비동기 방식으로 처리합니다.
흐름에서 데이터를 가져오는 것을 흐름에서 수집이라고 합니다. UI 레이어의 흐름에서 수집할 때는 몇 가지 사항을 고려해야 합니다.
- 구성 변경과 같은 수명 주기 이벤트(예: 기기 회전)로 인해 활동이 다시 생성됩니다. 따라서
Flow에서 재구성 및 수집이 다시 이루어집니다. - 기존 데이터가 수명 주기 이벤트 간에 손실되지 않도록 값을 상태로 캐시하려고 합니다.
- Flow는 컴포저블의 수명 주기가 종료된 후와 같이 남은 관찰자가 없는 경우 취소해야 합니다.
ViewModel에서 Flow를 노출할 때 권장되는 방법은 StateFlow를 사용하는 것입니다. StateFlow를 사용하면 UI 수명 주기와 관계없이 데이터를 저장하고 관찰할 수 있습니다. Flow를 StateFlow로 변환하려면 stateIn 연산자를 사용하세요.
stateIn 연산자에는 아래 설명된 세 가지 매개변수가 있습니다.
scope:viewModelScope가StateFlow의 수명 주기를 정의합니다.viewModelScope가 취소되면StateFlow도 취소됩니다.started: 파이프라인은 UI가 표시되는 경우에만 활성화해야 합니다.SharingStarted.WhileSubscribed()를 사용하면 됩니다. 마지막 구독자의 사라짐과 공유 코루틴 중지 사이의 지연(밀리초)을 구성하려면TIMEOUT_MILLIS를SharingStarted.WhileSubscribed()메서드에 전달합니다.initialValue: 상태 흐름의 초깃값을HomeUiState()로 설정합니다.
Flow를 StateFlow로 변환한 후에는 collectAsState() 메서드를 사용하여 수집할 수 있으므로 데이터를 동일한 유형의 State로 변환할 수 있습니다.
이 단계에서는 Room 데이터베이스의 모든 항목을 UI 상태의 관찰 가능한 StateFlow API로 검색합니다. Room 인벤토리 데이터가 변경되면 UI가 자동으로 업데이트됩니다.
ui/home/HomeViewModel.kt파일을 엽니다. 여기에는 항목 목록이 생성자 매개변수로 포함된HomeUiState데이터 클래스와TIMEOUT_MILLIS상수가 포함되어 있습니다.
// 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())
HomeViewModel클래스 내에서StateFlow<HomeUiState>유형의homeUiState라는val을 선언합니다. 초기화 오류는 곧 해결됩니다.
val homeUiState: StateFlow<HomeUiState>
itemsRepository에서getAllItemsStream()을 호출하여 방금 선언한homeUiState에 할당합니다.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream()
이제 '해결되지 않은 참조: itemsRepository'라는 오류가 표시됩니다. 해결되지 않은 참조 오류를 해결하려면 ItemsRepository 객체를 HomeViewModel에 전달해야 합니다.
ItemsRepository유형의 생성자 매개변수를HomeViewModel클래스에 추가합니다.
import com.example.inventory.data.ItemsRepository
class HomeViewModel(itemsRepository: ItemsRepository): ViewModel() {
ui/AppViewModelProvider.kt파일의HomeViewModel이니셜라이저에서 다음과 같이ItemsRepository객체를 전달합니다.
initializer {
HomeViewModel(inventoryApplication().container.itemsRepository)
}
HomeViewModel.kt파일로 돌아갑니다. 유형 불일치 오류가 있습니다. 이 문제를 해결하려면 다음과 같이 변환 맵을 추가하세요.
val homeUiState: StateFlow<HomeUiState> =
itemsRepository.getAllItemsStream().map { HomeUiState(it) }
Android 스튜디오에 여전히 유형 불일치 오류가 표시됩니다. 이 오류는 homeUiState의 유형이 StateFlow이고 getAllItemsStream()이 Flow를 반환하기 때문입니다.
stateIn연산자를 사용하여Flow를StateFlow로 변환합니다.StateFlow는 UI 상태의 관찰 가능한 API로, UI가 자체적으로 업데이트할 수 있도록 합니다.
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()
)
- 앱을 빌드하여 코드에 오류가 없는지 확인합니다. 시각적 변경사항은 없습니다.
4. 인벤토리 데이터 표시
이 작업에서는 HomeScreen에서 UI 상태를 수집하고 업데이트합니다.
HomeScreen.kt파일의 구성 가능한HomeScreen함수에서HomeViewModel유형의 새 함수 매개변수를 추가하고 초기화합니다.
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)
)
- 구성 가능한
HomeScreen함수에서homeUiState라는val을 추가하여HomeViewModel에서 UI 상태를 수집합니다. 이StateFlow에서 값을 수집하고 최신 값을State를 통해 나타내는collectAsState()를 사용합니다.
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
val homeUiState by viewModel.homeUiState.collectAsState()
HomeBody()함수 호출을 업데이트하고homeUiState.itemList를itemList매개변수에 전달합니다.
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier.padding(innerPadding)
)
- 앱을 실행합니다. 앱 데이터베이스에 항목을 저장한 경우 인벤토리 목록이 표시됩니다. 목록이 비어 있다면 일부 인벤토리 항목을 앱 데이터베이스에 추가하세요.

5. 데이터베이스 테스트
이전 Codelab에서는 코드 테스트의 중요성을 설명했습니다. 이 작업에서는 DAO 쿼리를 테스트하는 단위 테스트를 추가하고 Codelab을 진행하면서 테스트를 더 추가합니다.
데이터베이스 구현을 테스트하는 데 권장되는 접근 방식은 Android 기기에서 실행되는 JUnit 테스트를 작성하는 것입니다. 이러한 테스트에서는 활동을 만들 필요가 없으므로 UI 테스트보다 실행 속도가 더 빠릅니다.
build.gradle.kts (Module :app)파일에서 Espresso 및 JUnit의 종속 항목을 확인합니다.
// Testing
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
- Project 뷰로 전환하고 src를 마우스 오른쪽 버튼으로 클릭한 후 New > Directory를 클릭하여 테스트용 테스트 소스 세트를 만듭니다.

- New Directory 팝업에서 androidTest/kotlin을 선택합니다.

- Kotlin 클래스
ItemDaoTest.kt를 만듭니다. ItemDaoTest클래스에@RunWith(AndroidJUnit4::class)주석을 답니다. 이제 클래스가 다음 코드 예와 비슷하게 표시됩니다.
package com.example.inventory
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ItemDaoTest {
}
- 클래스 내에서
ItemDao및InventoryDatabase유형의 비공개var변수를 추가합니다.
import com.example.inventory.data.InventoryDatabase
import com.example.inventory.data.ItemDao
private lateinit var itemDao: ItemDao
private lateinit var inventoryDatabase: InventoryDatabase
- 데이터베이스를 만드는 함수를 추가하고
@Before로 주석을 달아 모든 테스트 전에 실행될 수 있도록 합니다. - 메서드 내에서
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()
}
이 함수에서는 메모리 내 데이터베이스를 사용하며 이를 디스크에 유지하지 않습니다. 이를 위해 inMemoryDatabaseBuilder() 함수를 사용합니다. 이렇게 하는 이유는 프로세스가 종료될 때 정보가 유지되는 것이 아니라 삭제되어야 하기 때문입니다. 테스트용으로 기본 스레드에서 .allowMainThreadQueries()를 사용하여 DAO 쿼리를 실행합니다.
- 데이터베이스를 닫는 다른 함수를 추가합니다.
@After로 주석을 달아 데이터베이스를 닫고 모든 테스트 후에 이 함수를 실행합니다.
import org.junit.After
import java.io.IOException
@After
@Throws(IOException::class)
fun closeDb() {
inventoryDatabase.close()
}
- 다음 코드 예와 같이 데이터베이스에서 사용할
ItemDaoTest클래스의 항목을 선언합니다.
import com.example.inventory.data.Item
private var item1 = Item(1, "Apples", 10.0, 20)
private var item2 = Item(2, "Bananas", 15.0, 97)
- 데이터베이스에 항목 하나를 추가한 후 항목 두 개를 추가하는 유틸리티 함수를 추가합니다. 나중에 이러한 함수를 테스트에서 사용합니다. 코루틴에서 실행될 수 있도록
suspend로 표시합니다.
private suspend fun addOneItemToDb() {
itemDao.insert(item1)
}
private suspend fun addTwoItemsToDb() {
itemDao.insert(item1)
itemDao.insert(item2)
}
- 단일 항목을 데이터베이스에 삽입하는 테스트
insert()를 작성합니다. 테스트 이름을daoInsert_insertsItemIntoDB로 지정하고@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)
}
이 테스트에서는 유틸리티 함수 addOneItemToDb()를 사용하여 데이터베이스에 항목을 하나 추가합니다. 그런 다음 데이터베이스의 첫 번째 항목을 읽습니다. assertEquals()를 사용하면 예상값을 실제 값과 비교합니다. runBlocking{}을 사용하여 새 코루틴에서 테스트를 실행합니다. 이 설정으로 인해 유틸리티 함수를 suspend로 표시합니다.
- 테스트를 실행하고 통과하는지 확인합니다.


- 데이터베이스에서
getAllItems()의 다른 테스트를 작성합니다. 테스트 이름을daoGetAllItems_returnsAllItemsFromDB로 지정합니다.
@Test
@Throws(Exception::class)
fun daoGetAllItems_returnsAllItemsFromDB() = runBlocking {
addTwoItemsToDb()
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], item1)
assertEquals(allItems[1], item2)
}
위 테스트에서는 코루틴 내에서 데이터베이스에 항목을 두 개 추가합니다. 그런 다음 두 항목을 읽고 예상값과 비교합니다.
6. 항목 세부정보 표시
이 작업에서는 Items Details 화면에서 항목 세부정보를 읽고 표시합니다. Inventory 앱 데이터베이스에서 이름, 가격, 수량 등 항목 UI 상태를 사용하고, ItemDetailsScreen 컴포저블을 사용하여 Item Details 화면에 표시합니다. 구성 가능한 함수 ItemDetailsScreen은 미리 작성되어 있으며 항목 세부정보를 표시하는 구성 가능한 함수 Text 세 개를 포함합니다.
ui/item/ItemDetailsScreen.kt
이 화면은 시작 코드의 일부이며 항목의 세부정보를 표시합니다. 이는 이후 Codelab에서 확인할 수 있습니다. 이 Codelab에서는 이 화면으로 작업하지 않습니다. ItemDetailsViewModel.kt는 이 화면에 상응하는 ViewModel입니다.

- 구성 가능한 함수
HomeScreen에서HomeBody()함수 호출을 확인합니다.navigateToItemUpdate는 목록에서 항목을 클릭할 때 호출되는onItemClick매개변수에 전달됩니다.
// No need to copy over
HomeBody(
itemList = homeUiState.itemList,
onItemClick = navigateToItemUpdate,
modifier = modifier
.padding(innerPadding)
.fillMaxSize()
)
ui/navigation/InventoryNavGraph.kt를 열고HomeScreen컴포저블에서navigateToItemUpdate매개변수를 확인합니다. 이 매개변수는 탐색 대상을 항목 세부정보 화면으로 지정합니다.
// No need to copy over
HomeScreen(
navigateToItemEntry = { navController.navigate(ItemEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${ItemDetailsDestination.route}/${it}")
}
onItemClick 기능의 이 부분은 이미 구현되어 있습니다. 목록 항목을 클릭하면 앱이 항목 세부정보 화면으로 이동합니다.
- 인벤토리 목록에서 항목을 클릭하면 필드가 비어 있는 항목 세부정보 화면이 표시됩니다.

텍스트 필드를 항목 세부정보로 채우려면 ItemDetailsScreen()에서 UI 상태를 수집해야 합니다.
UI/Item/ItemDetailsScreen.kt에서ItemDetailsViewModel유형의ItemDetailsScreen컴포저블에 새 매개변수를 추가하고 팩토리 메서드를 사용하여 초기화합니다.
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)
)
ItemDetailsScreen()컴포저블 내에서uiState라는val을 만들어 UI 상태를 수집합니다.collectAsState()를 사용하여uiStateStateFlow를 수집하고State를 통해 최신 값을 나타냅니다. Android 스튜디오에 해결되지 않은 참조 오류가 표시됩니다.
import androidx.compose.runtime.collectAsState
val uiState = viewModel.uiState.collectAsState()
- 이 오류를 해결하려면
ItemDetailsViewModel클래스에서StateFlow<ItemDetailsUiState>유형의uiState라는val을 만듭니다. - 항목 저장소에서 데이터를 가져와 확장 함수
toItemDetails()를 사용하여ItemDetailsUiState에 매핑합니다. 확장 함수Item.toItemDetails()는 시작 코드의 일부로 이미 작성되어 있습니다.
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()
)
ItemsRepository를ItemDetailsViewModel에 전달하여Unresolved reference: itemsRepository오류를 해결합니다.
class ItemDetailsViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
) : ViewModel() {
ui/AppViewModelProvider.kt에서 다음 코드 스니펫과 같이ItemDetailsViewModel의 이니셜라이저를 업데이트합니다.
initializer {
ItemDetailsViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
ItemDetailsScreen.kt로 돌아가서ItemDetailsScreen()컴포저블의 오류가 해결되었음을 확인합니다.ItemDetailsScreen()컴포저블에서ItemDetailsBody()함수 호출을 업데이트하고uiState.value를itemUiState인수에 전달합니다.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
ItemDetailsBody()및ItemInputForm()의 구현을 확인합니다. 현재 선택된item을ItemDetailsBody()에서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()
)
//...
}
- 앱을 실행합니다. Inventory 화면에서 목록 요소를 클릭하면 Item Details 화면이 표시됩니다.
- 화면이 더 이상 비어 있지 않습니다. 인벤토리 데이터베이스에서 가져온 항목 세부정보가 표시됩니다.

- Sell(판매) 버튼을 탭합니다. 아무 일도 일어나지 않습니다.
다음 섹션에서는 Sell 버튼의 기능을 구현합니다.
7. 항목 세부정보 구현 화면
ui/item/ItemEditScreen.kt
항목 수정 화면은 이미 시작 코드의 일부로 제공되어 있습니다.
이 레이아웃에는 새 인벤토리 항목의 세부정보를 수정할 수 있는 텍스트 필드 컴포저블이 포함되어 있습니다.

이 앱의 코드는 아직 완전히 작동하지 않습니다. 예를 들어 Item Details 화면에서 Sell 버튼을 탭해도 Quantity in Stock이 감소하지 않습니다. Delete 버튼을 탭하면 앱에 확인 대화상자가 표시됩니다. 하지만 Yes 버튼을 선택해도 앱은 실제로 항목을 삭제하지 않습니다.

이제 FAB 버튼
을 누르면 빈 Edit Item(항목 수정) 화면이 열립니다.

이 섹션에서는 Sell 버튼과 Delete 버튼, FAB 버튼의 기능을 구현합니다.
8. 판매 항목 구현
이 섹션에서는 앱의 기능을 확장하여 판매 기능을 구현합니다. 이 업데이트에는 다음 작업이 포함됩니다.
- 항목을 업데이트하는 DAO 함수의 테스트를 추가합니다.
ItemDetailsViewModel에 함수를 추가하여 수량을 줄이고 앱 데이터베이스의 항목을 업데이트합니다.- 수량이 0이면 Sell 버튼을 사용 중지합니다.
ItemDaoTest.kt에서 매개변수 없이daoUpdateItems_updatesItemsInDB()함수를 추가합니다.@Test및@Throws(Exception::class)주석을 답니다.
@Test
@Throws(Exception::class)
fun daoUpdateItems_updatesItemsInDB()
- 함수를 정의하고
runBlocking블록을 만듭니다. 블록 안에서addTwoItemsToDb()를 호출합니다.
fun daoUpdateItems_updatesItemsInDB() = runBlocking {
addTwoItemsToDb()
}
itemDao.update를 호출하여 값이 다른 두 항목을 업데이트합니다.
itemDao.update(Item(1, "Apples", 15.0, 25))
itemDao.update(Item(2, "Bananas", 5.0, 50))
itemDao.getAllItems()를 사용하여 항목을 가져옵니다. 가져온 항목을 업데이트된 항목과 비교하고 어설션합니다.
val allItems = itemDao.getAllItems().first()
assertEquals(allItems[0], Item(1, "Apples", 15.0, 25))
assertEquals(allItems[1], Item(2, "Bananas", 5.0, 50))
- 완성된 함수가 다음과 같은지 확인합니다.
@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))
}
- 테스트를 실행하고 통과하는지 확인합니다.
ViewModel에 함수 추가
ItemDetailsViewModel.kt의ItemDetailsViewModel클래스 내에서 매개변수 없이reduceQuantityByOne()함수를 추가합니다.
fun reduceQuantityByOne() {
}
- 함수 내에서
viewModelScope.launch{}를 사용하여 코루틴을 시작합니다.
import kotlinx.coroutines.launch
import androidx.lifecycle.viewModelScope
viewModelScope.launch {
}
launch블록 내에서currentItem이라는val을 만들고uiState.value.toItem()으로 설정합니다.
val currentItem = uiState.value.toItem()
uiState.value는 ItemUiState 유형입니다. 확장 함수 toItem()을 사용하여 Item 항목 유형으로 변환합니다.
if문을 추가하여quality가0보다 큰지 확인합니다.itemsRepository에서updateItem()을 호출하고 업데이트된currentItem을 전달합니다. 함수가 다음과 같이 표시되도록copy()를 사용하여quantity값을 업데이트합니다.
fun reduceQuantityByOne() {
viewModelScope.launch {
val currentItem = uiState.value.itemDetails.toItem()
if (currentItem.quantity > 0) {
itemsRepository.updateItem(currentItem.copy(quantity = currentItem.quantity - 1))
}
}
}
ItemDetailsScreen.kt로 돌아갑니다.ItemDetailsScreen컴포저블에서ItemDetailsBody()함수 호출로 이동합니다.onSellItem람다에서viewModel.reduceQuantityByOne()을 호출합니다.
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = { },
modifier = modifier.padding(innerPadding)
)
- 앱을 실행합니다.
- Inventory 화면에서 목록 요소를 클릭합니다. Item Details 화면이 표시되면 Sell을 탭하고 수량이 1씩 감소하는 것을 확인합니다.

- Item Details 화면에서 수량이 0이 될 때까지 Sell 버튼을 계속 탭합니다.
수량이 0에 도달하면 Sell을 다시 탭합니다. 시각적 변경사항이 없습니다. reduceQuantityByOne() 함수는 수량을 업데이트하기 전에 수량이 0보다 큰지 확인하기 때문입니다.

사용자에게 더 나은 피드백을 제공하려면 판매할 항목이 없을 때 Sell 버튼을 사용 중지하는 것이 좋습니다.
ItemDetailsViewModel클래스에서map변환의it.quantity에 따라outOfStock값을 설정합니다.
val uiState: StateFlow<ItemDetailsUiState> =
itemsRepository.getItemStream(itemId)
.filterNotNull()
.map {
ItemDetailsUiState(outOfStock = it.quantity <= 0, itemDetails = it.toItemDetails())
}.stateIn(
//...
)
- 앱을 실행합니다. 재고 수량이 0일 때 앱이 Sell 버튼을 사용 중지합니다.

축하합니다. 앱에 항목 판매 기능을 구현했습니다.
항목 삭제
이전 작업과 마찬가지로 삭제 기능을 구현하여 앱의 기능을 더 확장해야 합니다. 이 기능은 판매 기능보다 훨씬 쉽게 구현할 수 있습니다. 이 프로세스에는 다음 작업이 포함됩니다.
- DAO 삭제 쿼리 테스트를 추가합니다.
ItemDetailsViewModel클래스에 함수를 추가하여 데이터베이스에서 항목을 삭제합니다.ItemDetailsBody컴포저블을 업데이트합니다.
DAO 테스트 추가
ItemDaoTest.kt에서daoDeleteItems_deletesAllItemsFromDB()라는 테스트를 추가합니다.
@Test
@Throws(Exception::class)
fun daoDeleteItems_deletesAllItemsFromDB()
runBlocking {}을 사용하여 코루틴을 실행합니다.
fun daoDeleteItems_deletesAllItemsFromDB() = runBlocking {
}
- 데이터베이스에 항목 두 개를 추가하고 이 두 항목에서
itemDao.delete()를 호출하여 데이터베이스에서 항목을 삭제합니다.
addTwoItemsToDb()
itemDao.delete(item1)
itemDao.delete(item2)
- 데이터베이스에서 항목을 검색하고 목록이 비어 있는지 확인합니다. 완성된 테스트는 다음과 같습니다.
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())
}
ItemDetailsViewModel에 삭제 함수 추가
ItemDetailsViewModel에서 매개변수를 사용하지 않고 아무것도 반환하지 않는deleteItem()이라는 새 함수를 추가합니다.deleteItem()함수 내에서itemsRepository.deleteItem()함수 호출을 추가하고uiState.value.toItem()을 전달합니다.
suspend fun deleteItem() {
itemsRepository.deleteItem(uiState.value.itemDetails.toItem())
}
이 함수에서는 toItem() 확장 함수를 사용하여 uiState를 itemDetails 유형에서 Item 항목 유형으로 변환합니다.
ui/item/ItemDetailsScreen컴포저블에서coroutineScope라는val을 추가하고rememberCoroutineScope()로 설정합니다. 이 접근 방식은 호출되는 컴포지션에 바인딩된 코루틴 범위를 반환합니다(ItemDetailsScreen컴포저블).
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
ItemDetailsBody()함수로 스크롤합니다.onDelete람다 내에서coroutineScope를 사용하여 코루틴을 실행합니다.launch블록 내viewModel에서deleteItem()메서드를 호출합니다.
import kotlinx.coroutines.launch
ItemDetailsBody(
itemUiState = uiState.value,
onSellItem = { viewModel.reduceQuantityByOne() },
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
}
modifier = modifier.padding(innerPadding)
)
- 항목을 삭제한 후 인벤토리 화면으로 다시 이동합니다.
deleteItem()함수 호출 뒤에navigateBack()을 호출합니다.
onDelete = {
coroutineScope.launch {
viewModel.deleteItem()
navigateBack()
}
- 계속
ItemDetailsScreen.kt파일 내에서ItemDetailsBody()함수로 스크롤합니다.
이 함수는 시작 코드의 일부입니다. 이 컴포저블은 항목을 삭제하기 전에 사용자의 확인을 받는 알림 대화상자를 표시하고 Yes를 탭하면 deleteItem() 함수를 호출합니다.
// 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()
},
//...
)
}
}
}
No를 탭하면 앱에서 알림 대화상자를 닫습니다. showConfirmationDialog() 함수는 다음 알림을 표시합니다.

- 앱을 실행합니다.
- Inventory 화면에서 목록 요소를 선택합니다.
- Item Details 화면에서 Delete를 탭합니다.
- 알림 대화상자에서 Yes를 탭하면 앱이 Inventory 화면으로 다시 이동합니다.
- 삭제한 항목은 더 이상 앱 데이터베이스에 없습니다.
축하합니다. 삭제 기능을 구현했습니다.
|
|
항목 수정
이전 섹션과 마찬가지로 이 섹션에서는 항목을 수정하는 또 다른 기능 개선사항을 앱에 추가합니다.
다음은 앱 데이터베이스에서 항목을 수정하는 이 단계를 간략히 요약한 것입니다.
- 항목 가져오기 DAO 쿼리 테스트에 테스트를 추가합니다.
- 텍스트 필드와 Edit Item 화면을 항목 세부정보로 채웁니다.
- Room을 사용하여 데이터베이스의 항목을 업데이트합니다.
DAO 테스트 추가
ItemDaoTest.kt에서daoGetItem_returnsItemFromDB()라는 테스트를 추가합니다.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB()
- 함수를 정의합니다. 코루틴 내에서 데이터베이스에 항목 하나를 추가합니다.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
}
itemDao.getItem()함수를 사용하여 데이터베이스에서 항목을 가져와item이라는val로 설정합니다.
val item = itemDao.getItem(1)
- 실제 값을 가져온 값과 비교하고
assertEquals()를 사용하여 어설션합니다. 완성된 테스트는 다음과 같습니다.
@Test
@Throws(Exception::class)
fun daoGetItem_returnsItemFromDB() = runBlocking {
addOneItemToDb()
val item = itemDao.getItem(1)
assertEquals(item.first(), item1)
}
- 테스트를 실행하고 통과하는지 확인합니다.
텍스트 필드 채우기
앱을 실행하고 Item Details 화면으로 이동하여 FAB를 클릭하면 이제 화면 제목이 Edit Item인 것을 확인할 수 있습니다. 그러나 모든 텍스트 필드가 비어 있습니다. 이 단계에서는 Edit Item(항목 수정) 화면의 텍스트 필드를 항목 세부정보로 채웁니다.
|
|
ItemDetailsScreen.kt에서 구성 가능한 함수ItemDetailsScreen으로 스크롤합니다.FloatingActionButton()에서, 선택된 항목의id인uiState.value.itemDetails.id를 포함하도록onClick인수를 변경합니다. 이id를 사용하여 항목 세부정보를 검색합니다.
FloatingActionButton(
onClick = { navigateToEditItem(uiState.value.itemDetails.id) },
modifier = /*...*/
)
ItemEditViewModel클래스에서init블록을 추가합니다.
init {
}
init블록 내에서viewModelScope.launch를 사용하여 코루틴을 실행합니다.
import kotlinx.coroutines.launch
viewModelScope.launch { }
launch블록 내에서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)
}
}
이 출시 블록에서는 null이 아닌 값만 포함된 흐름을 반환하는 필터를 추가합니다. toItemUiState()를 사용하여 item 항목을 ItemUiState로 변환합니다. actionEnabled 값을 true로 전달하여 Save 버튼을 사용 설정합니다.
Unresolved reference: itemsRepository 오류를 해결하려면 ItemsRepository를 종속 항목으로 뷰 모델에 전달해야 합니다.
ItemEditViewModel클래스에 생성자 매개변수를 추가합니다.
class ItemEditViewModel(
savedStateHandle: SavedStateHandle,
private val itemsRepository: ItemsRepository
)
AppViewModelProvider.kt파일의ItemEditViewModel이니셜라이저에서ItemsRepository객체를 인수로 추가합니다.
initializer {
ItemEditViewModel(
this.createSavedStateHandle(),
inventoryApplication().container.itemsRepository
)
}
- 앱을 실행합니다.
- Item Details(항목 세부정보)로 이동하여
FAB를 탭합니다. - 필드가 항목 세부정보로 채워집니다.
- 재고 수량이나 다른 필드를 수정하고 Save를 탭합니다.
아무 일도 일어나지 않습니다. 앱 데이터베이스의 항목을 업데이트하지 않기 때문입니다. 다음 섹션에서 이 문제를 해결합니다.
|
|
Room을 사용하여 항목 업데이트
이 마지막 작업에서는 최종 코드를 추가하여 업데이트 기능을 구현합니다. ViewModel에서 필요한 함수를 정의하고 이를 ItemEditScreen에서 사용합니다.
이제 코드를 작성해보겠습니다.
ItemEditViewModel클래스에서ItemUiState객체를 사용하고 아무것도 반환하지 않는updateUiState()함수를 추가합니다. 이 함수는 사용자가 입력하는 새 값으로itemUiState를 업데이트합니다.
fun updateUiState(itemDetails: ItemDetails) {
itemUiState =
ItemUiState(itemDetails = itemDetails, isEntryValid = validateInput(itemDetails))
}
이 함수에서, 전달된 itemDetails를 itemUiState에 할당하고 isEntryValid 값을 업데이트합니다. 앱에서는 itemDetails가 true이면 Save 버튼을 사용 설정합니다. 이 값은 사용자가 입력한 내용이 유효한 경우에만 true로 설정합니다.
ItemEditScreen.kt파일로 이동합니다.ItemEditScreen컴포저블에서ItemEntryBody()함수 호출까지 아래로 스크롤합니다.onItemValueChange인수 값을 새 함수updateUiState로 설정합니다.
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = { },
modifier = modifier.padding(innerPadding)
)
- 앱을 실행합니다.
- Edit Item 화면으로 이동합니다.
- 항목 값 중 하나를 비워 유효하지 않도록 합니다. Save 버튼이 자동으로 사용 중지되는 방식을 확인하세요.
|
|
|
ItemEditViewModel클래스로 돌아가서 아무것도 사용하지 않는updateItem()이라는suspend함수를 추가합니다. 이 함수를 사용하여 업데이트된 항목을 Room 데이터베이스에 저장합니다.
suspend fun updateItem() {
}
getUpdatedItemEntry()함수 내에서validateInput()함수를 사용하여 사용자 입력을 검증하는if조건을 추가합니다.itemsRepository에서updateItem()함수를 호출하여itemUiState.itemDetails.toItem()을 전달합니다. Room 데이터베이스에 추가할 수 있는 항목은Item유형이어야 합니다. 완성된 함수는 다음과 같습니다.
suspend fun updateItem() {
if (validateInput(itemUiState.itemDetails)) {
itemsRepository.updateItem(itemUiState.itemDetails.toItem())
}
}
ItemEditScreen컴포저블로 돌아갑니다.updateItem()함수를 호출하려면 코루틴 범위가 필요합니다.coroutineScope라는 val을 만들고rememberCoroutineScope()로 설정합니다.
import androidx.compose.runtime.rememberCoroutineScope
val coroutineScope = rememberCoroutineScope()
ItemEntryBody()함수 호출에서onSaveClick함수 인수를 업데이트하여coroutineScope에서 코루틴을 시작합니다.launch블록 내viewModel에서updateItem()을 호출하고 뒤로 이동합니다.
import kotlinx.coroutines.launch
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
완성된 ItemEntryBody() 함수 호출은 다음과 같습니다.
ItemEntryBody(
itemUiState = viewModel.itemUiState,
onItemValueChange = viewModel::updateUiState,
onSaveClick = {
coroutineScope.launch {
viewModel.updateItem()
navigateBack()
}
},
modifier = modifier.padding(innerPadding)
)
- 앱을 실행하고 인벤토리 항목을 수정해 봅니다. 이제 Inventory 앱 데이터베이스의 모든 항목을 수정할 수 있습니다.
|
|
축하합니다. Room을 사용하여 데이터베이스를 관리하는 첫 번째 앱을 만들었습니다.
9. 솔루션 코드
이 Codelab의 솔루션 코드는 아래와 같이 GitHub 저장소와 브랜치에 있습니다.
10. 자세히 알아보기
Android 개발자 문서
Kotlin 참조








