1. 시작하기 전에
이전 Codelab에서는 SQLite 데이터베이스 위의 추상화 레이어인 Room 지속성 라이브러리를 사용하여 앱 데이터를 저장하는 방법을 알아봤습니다. 이 Codelab에서는 Inventory 앱에 기능을 더 추가하고 Room을 사용하여 SQLite 데이터베이스의 데이터를 읽고 표시하고 업데이트하며 삭제하는 방법을 알아봅니다. RecyclerView
를 사용하여 데이터베이스의 데이터를 표시하고 데이터베이스의 기본 데이터가 변경될 때 자동으로 데이터를 업데이트합니다.
기본 요건
- Room 라이브러리를 사용하여 SQLite 데이터베이스를 만들고 상호작용하는 방법을 알아야 합니다.
- 항목, DAO, 데이터베이스 클래스를 만드는 방법을 알아야 합니다.
- 데이터 액세스 객체(DAO)를 사용하여 Kotlin 함수를 SQL 쿼리에 매핑하는 방법을 알아야 합니다.
RecyclerView
에 목록 항목을 표시하는 방법을 알아야 합니다.- 이 단원의 이전 Codelab인 Room을 사용하여 데이터 유지를 확인해야 합니다.
학습할 내용
- SQLite 데이터베이스의 항목을 읽고 표시하는 방법
- Room 라이브러리를 사용하여 SQLite 데이터베이스의 항목을 업데이트하고 삭제하는 방법
빌드할 항목
- 인벤토리 항목 목록을 표시하는 Inventory 앱을 빌드합니다. 앱은 Room을 사용하여 앱 데이터베이스의 항목을 업데이트하고 수정하며 삭제할 수 있습니다.
2. 스타터 앱 개요
이 Codelab에서는 이전 Codelab의 Inventory 앱 솔루션 코드를 시작 코드로 사용합니다. 시작 앱은 이미 Room 지속성 라이브러리를 사용하여 데이터를 저장합니다. 사용자는 Add Item 화면을 사용하여 앱 데이터베이스에 데이터를 추가할 수 있습니다.
참고: 현재 버전의 시작 앱에는 데이터베이스에 저장된 날짜가 표시되지 않습니다.
이 Codelab에서는 앱을 확장하여 데이터를 읽고 표시하고 Room 라이브러리를 사용하여 데이터베이스의 항목을 업데이트하고 삭제합니다.
이 Codelab의 시작 코드 다운로드
이 시작 코드는 이전 Codelab의 솔루션 코드와 같습니다.
이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.
코드 가져오기
- 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
- 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.
- 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
- 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
- ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.
Android 스튜디오에서 프로젝트 열기
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.
참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.
- Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
- 프로젝트 폴더를 더블클릭합니다.
- Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
- Run 버튼 을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
- Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 구현된 방식을 확인합니다.
3. RecyclerView 추가
이 작업에서는 RecyclerView
를 앱에 추가하여 데이터베이스에 저장된 데이터를 표시합니다.
가격 형식을 지정하는 도우미 함수 추가
다음은 최종 앱의 스크린샷입니다.
가격이 통화 형식으로 표시되어 있습니다. double 값을 원하는 통화 형식으로 변환하려면 확장 함수를 Item
클래스에 추가합니다.
확장 함수
Kotlin은 클래스에서 상속받거나 기존 클래스 정의를 수정하지 않고도 새로운 기능으로 클래스를 확장하는 기능을 제공합니다. 즉, 소스 코드에 액세스하지 않고도 함수를 기존 클래스에 추가할 수 있습니다. 확장이라는 특수 선언을 통해 실행됩니다.
예를 들어 수정할 수 없는 타사 라이브러리의 클래스에 관한 새 함수를 작성할 수 있습니다. 이러한 함수는 원래 클래스의 메서드인 것처럼 일반적인 방식으로 호출할 수 있습니다. 이러한 함수를 확장 함수라고 합니다. 기존 클래스의 새 속성을 정의할 수 있는 확장 속성도 있지만 이 Codelab의 범위를 벗어나는 내용입니다.
확장 함수는 실제로 클래스를 수정하지는 않지만 클래스의 객체에서 함수를 호출할 때 점 표기법을 사용할 수 있도록 합니다.
예를 들어 다음 코드 스니펫에는 Square
라는 클래스가 있습니다. 이 클래스에는 변의 속성과 정사각형의 면적을 계산하는 함수가 있습니다. Square.perimeter()
확장 함수를 살펴보세요. 이 함수 이름에는 함수가 작동하는 클래스가 앞에 붙습니다. 함수 내에서 Square
클래스의 공개 속성을 참조할 수 있습니다.
main()
함수에서 확장 함수 사용법을 확인합니다. 만들어진 확장 함수인 perimeter()
는 Square
클래스 내에서 일반 함수로 호출됩니다.
예:
class Square(val side: Double){
fun area(): Double{
return side * side;
}
}
// Extension function to calculate the perimeter of the square
fun Square.perimeter(): Double{
return 4 * side;
}
// Usage
fun main(args: Array<String>){
val square = Square(5.5);
val perimeterValue = square.perimeter()
println("Perimeter: $perimeterValue")
val areaValue = square.area()
println("Area: $areaValue")
}
이 단계에서는 항목 가격의 형식을 통화 형식 문자열로 지정합니다. 일반적으로 단지 데이터의 형식을 지정하려고 데이터를 나타내는 항목 클래스를 변경하지는 않으므로(단일 책임 원칙 참고) 대신 확장 함수를 추가합니다.
Item.kt
에서 클래스 정의 아래에 매개변수를 사용하지 않고String
을 반환하는Item.getFormattedPrice()
라는 확장 함수를 추가합니다. 함수 이름에서 클래스 이름과 점 표기법을 확인합니다.
fun Item.getFormattedPrice(): String =
NumberFormat.getCurrencyInstance().format(itemPrice)
Android 스튜디오에서 메시지가 표시되면 java.text.NumberFormat
을 가져옵니다.
ListAdapter 추가
이 단계에서는 목록 어댑터를 RecyclerView
에 추가합니다. 이전 Codelab을 통해 어댑터 구현을 잘 알고 있으므로 아래에 안내가 요약되어 있습니다. 완성된 ItemListAdapter
파일은 편의를 위해 이 단계의 끝부분에 있으며 Codelab의 Room 개념에 관한 이해를 높이는 데 유용합니다.
com.example.inventory
패키지에서ItemListAdapter
라는 Kotlin 클래스를 추가합니다.onItemClicked()
라는 함수를Item
객체를 매개변수로 가져오는 생성자 매개변수로 전달합니다.ItemListAdapter
클래스 서명을 변경하여ListAdapter
를 확장합니다.Item
과ItemListAdapter.ItemViewHolder
를 매개변수로 전달합니다.- 생성자 매개변수
DiffCallback
을 추가합니다.ListAdapter
는 이를 사용하여 목록에서 변경된 내용을 파악합니다. - 필수 메서드
onCreateViewHolder()
와onBindViewHolder()
를 재정의합니다. onCreateViewHolder()
메서드는 RecyclerView에 필요할 때 새ViewHolder
를 반환합니다.onCreateViewHolder()
메서드 내에서 새View
를 만들고 자동 생성된 바인딩 클래스ItemListItemBinding
을 사용하여item_list_item.xml
레이아웃 파일에서 이를 확장합니다.onBindViewHolder()
메서드를 구현합니다.getItem()
메서드를 사용하여 현재 항목을 가져와 위치를 전달합니다.itemView
에서 클릭 리스너를 설정하고 리스너 내에서onItemClicked()
함수를 호출합니다.ItemViewHolder
클래스를 정의하고RecyclerView.ViewHolder.
에서 확장합니다.bind()
함수를 재정의하고Item
객체를 전달합니다.- 컴패니언 객체를 정의합니다. 컴패니언 객체 내에서
DiffCallback
이라는DiffUtil.ItemCallback<Item>()
유형의val
을 정의합니다. 필수 메서드areItemsTheSame()
과areContentsTheSame()
을 재정의하고 정의합니다.
완성된 클래스는 다음과 같습니다.
package com.example.inventory
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.inventory.data.Item
import com.example.inventory.data.getFormattedPrice
import com.example.inventory.databinding.ItemListItemBinding
/**
* [ListAdapter] implementation for the recyclerview.
*/
class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
return ItemViewHolder(
ItemListItemBinding.inflate(
LayoutInflater.from(
parent.context
)
)
)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val current = getItem(position)
holder.itemView.setOnClickListener {
onItemClicked(current)
}
holder.bind(current)
}
class ItemViewHolder(private var binding: ItemListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
}
}
companion object {
private val DiffCallback = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.itemName == newItem.itemName
}
}
}
}
완성된 앱(이 Codelab의 끝에 있는 솔루션 앱)에서 인벤토리 목록 화면을 확인합니다. 모든 목록 요소에 인벤토리 항목 이름과 통화 형식의 가격, 사용할 수 있는 현재 재고가 표시되어 있습니다. 이전 단계에서는 TextView가 세 개 있는 item_list_item.xml
레이아웃 파일을 사용하여 행을 만들었습니다. 다음 단계에서는 항목 세부정보를 이러한 TextView에 바인딩합니다.
ItemListAdapter.kt
의ItemViewHolder
클래스에서bind()
함수를 구현합니다.itemName
TextView를item.itemName
에 바인딩합니다.getFormattedPrice()
확장 함수를 사용하여 통화 형식의 가격을 가져와itemPrice
TextView에 바인딩합니다.quantityInStock
값을String
으로 변환하여itemQuantity
TextView에 바인딩합니다. 완성된 메서드는 다음과 같습니다.
fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemQuantity.text = item.quantityInStock.toString()
}
}
Android 스튜디오에서 메시지가 표시되면 com.example.inventory.data.getFormattedPrice
를 가져옵니다.
ListAdapter 사용
이 작업에서는 이전 단계에서 만든 목록 어댑터를 사용하여 화면에 항목 세부정보를 표시하도록 InventoryViewModel
과 ItemListFragment
를 업데이트합니다.
InventoryViewModel
클래스 시작 부분에 데이터베이스의 항목에 관한LiveData<List<Item>>
유형의allItems
라는val
을 만듭니다. 오류는 걱정하지 마세요. 곧 수정합니다.
val allItems: LiveData<List<Item>>
Android 스튜디오에서 메시지가 표시되면 androidx.lifecycle.LiveData
를 가져옵니다.
itemDao
에서getItems()
를 호출하여allItems
에 할당합니다.getItems()
함수는Flow
를 반환합니다. 데이터를LiveData
값으로 사용하려면asLiveData()
함수를 사용합니다. 완성된 정의는 다음과 같습니다.
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()
Android 스튜디오에서 메시지가 표시되면 androidx.lifecycle.asLiveData
를 가져옵니다.
ItemListFragment
에서 클래스 시작 부분에InventoryViewModel
유형의viewModel
이라는 변경 불가능한private
속성을 선언합니다.by
위임을 사용하여 속성 초기화를activityViewModels
클래스에 전달합니다.InventoryViewModelFactory
생성자를 전달합니다.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Android 스튜디오에서 요청하면 androidx.fragment.app.activityViewModels
를 가져옵니다.
- 여전히
ItemListFragment
내에서onViewCreated()
함수로 스크롤합니다.super.onViewCreated()
호출 아래에서adapter
라는val
을 선언합니다. 아무것도 전달하지 않는 기본 생성자ItemListAdapter{}
를 사용하여 새adapter
속성을 초기화합니다. - 새로 만든
adapter
를 다음과 같이recyclerView
에 바인딩합니다.
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
- 어댑터를 설정한 후에도 여전히
onViewCreated()
내에 있습니다.allItems
에 관찰자를 연결하여 데이터 변경사항을 수신 대기합니다. - 관찰자 내
adapter
에서submitList()
를 호출하고 새 목록을 전달합니다. 그러면 새 항목이 목록에 포함되어 RecyclerView가 업데이트됩니다.
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
items.let {
adapter.submitList(it)
}
}
- 완성된
onViewCreated()
메서드가 다음과 같이 표시되는지 확인합니다. 앱을 실행합니다. 항목이 앱 데이터베이스에 저장된 경우 인벤토리 목록이 표시됩니다. 목록이 비어 있으면 앱 데이터베이스에 인벤토리 항목을 추가합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = ItemListAdapter {
}
binding.recyclerView.adapter = adapter
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
items.let {
adapter.submitList(it)
}
}
binding.recyclerView.layoutManager = LinearLayoutManager(this.context)
binding.floatingActionButton.setOnClickListener {
val action = ItemListFragmentDirections.actionItemListFragmentToAddItemFragment(
getString(R.string.add_fragment_title)
)
this.findNavController().navigate(action)
}
}
4. 항목 세부정보 표시
이 작업에서는 Items Details 화면에서 항목 세부정보를 읽고 표시합니다. 기본 키(항목 id
)를 사용하여 인벤토리 앱 데이터베이스에서 이름과 가격, 수량 등 세부정보를 읽고 fragment_item_detail.xml
레이아웃 파일을 사용하여 Item Details 화면에 표시합니다. 레이아웃 파일 fragment_item_detail.xml
은 개발자를 위해 미리 디자인되었으며 항목 세부정보가 표시되는 TextView 세 개를 포함합니다.
이 작업에서는 다음 단계를 구현합니다.
- 클릭 핸들러를 RecyclerView에 추가하여 앱에서 Item Details 화면으로 이동합니다.
ItemListFragment
프래그먼트에서 데이터베이스의 데이터를 검색하고 표시합니다.- TextView를 ViewModel 데이터에 바인딩합니다.
클릭 핸들러 추가
ItemListFragment
에서onViewCreated()
함수로 스크롤하여 어댑터 정의를 업데이트합니다.- 람다를 생성자 매개변수로
ItemListAdapter{}
에 추가합니다.
val adapter = ItemListAdapter {
}
- 람다 내에서
action
이라는val
을 만듭니다. 초기화 오류는 곧 수정합니다.
val adapter = ItemListAdapter {
val action
}
- 항목
id
를 전달하는ItemListFragmentDirections
에서actionItemListFragmentToItemDetailFragment()
메서드를 호출합니다. 반환된NavDirections
객체를action
에 할당합니다.
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
}
action
정의 아래에서this.
findNavController
()
를 사용하여NavController
인스턴스를 검색하고action
을 전달하는 이 인스턴스에서navigate()
를 호출합니다. 어댑터 정의는 다음과 같습니다.
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
this.findNavController().navigate(action)
}
- 앱을 실행합니다.
RecyclerView
에서 항목을 클릭합니다. 앱이 Item Details 화면으로 이동합니다. 세부정보가 비어 있습니다. 버튼을 탭해도 아무 일도 일어나지 않습니다.
이후 단계에서 항목 세부정보를 Item Details 화면에 표시하고 판매 및 삭제 버튼 기능을 추가합니다.
항목 세부정보 검색
이 단계에서는 새 함수를 InventoryViewModel
에 추가하여 항목 id
에 따라 데이터베이스에서 항목 세부정보를 검색합니다. 다음 단계에서는 이 함수를 사용하여 항목 세부정보를 Item Details 화면에 표시합니다.
InventoryViewModel
에서 항목 id의Int
를 사용하고LiveData<Item>
을 반환하는retrieveItem()
이라는 함수를 추가합니다. 반환 표현식 오류는 곧 수정합니다.
fun retrieveItem(id: Int): LiveData<Item> {
}
- 새 함수 내
itemDao
에서getItem()
을 호출하여id
매개변수를 전달합니다.getItem()
함수는Flow
를 반환합니다.Flow
값을LiveData
로 사용하려면asLiveData()
함수를 호출하여 이를retrieveItem()
함수의 반환 값으로 사용합니다. 완성된 함수는 다음과 같습니다.
fun retrieveItem(id: Int): LiveData<Item> {
return itemDao.getItem(id).asLiveData()
}
TextView에 데이터 바인딩
이 단계에서는 ItemDetailFragment
에서 ViewModel 인스턴스를 만들고 ViewModel 데이터를 Item Details 화면의 TextView에 바인딩합니다. 또한 데이터베이스의 기본 데이터가 변경되면 ViewModel의 데이터에 관찰자를 연결하여 화면에서 인벤토리 목록을 계속 업데이트합니다.
ItemDetailFragment
에서Item
항목 유형의item
이라는 변경 가능한 속성을 추가합니다. 이 속성을 사용하여 단일 항목에 관한 정보를 저장합니다. 이 속성은 나중에 초기화되므로 앞에lateinit
를 붙입니다.
lateinit var item: Item
Android 스튜디오에서 메시지가 표시되면 com.example.inventory.data.Item
을 가져옵니다.
ItemDetailFragment
클래스 시작 부분에서InventoryViewModel
유형의viewModel
이라는 변경 불가능한private
속성을 선언합니다.by
위임을 사용하여 속성 초기화를activityViewModels
클래스에 전달합니다.InventoryViewModelFactory
생성자를 전달합니다.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
Android 스튜디오에서 메시지가 표시되면 androidx.fragment.app.activityViewModels
를 가져옵니다.
- 계속
ItemDetailFragment
에서Item
항목의 인스턴스를 매개변수로 사용하고 아무것도 반환하지 않는bind()
라는private
함수를 만듭니다.
private fun bind(item: Item) {
}
bind()
함수를 구현합니다. 이는ItemListAdapter
에서 한 작업과 비슷합니다.itemName
TextView의text
속성을item.itemName
으로 설정합니다.item
속성에서getFormattedPrice
()
를 호출하여 가격 값의 형식을 지정하고itemPrice
TextView의text
속성으로 설정합니다.quantityInStock
을String
으로 변환하고itemQuantity
TextView의text
속성으로 설정합니다.
private fun bind(item: Item) {
binding.itemName.text = item.itemName
binding.itemPrice.text = item.getFormattedPrice()
binding.itemCount.text = item.quantityInStock.toString()
}
- 다음과 같이 코드 블록에
apply{}
범위 함수를 사용하도록bind()
함수를 업데이트합니다.
private fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemCount.text = item.quantityInStock.toString()
}
}
- 여전히
ItemDetailFragment
에서onViewCreated()
를 재정의합니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
- 이전 단계 중 하나에서 항목 id를 탐색 인수로
ItemListFragment
의ItemDetailFragment
에 전달했습니다.onViewCreated()
내에서 슈퍼 클래스 함수 호출 아래에id
라는 변경 불가능한 변수를 만듭니다. 탐색 인수를 검색하여 이 새로운 변수에 할당합니다.
val id = navigationArgs.itemId
- 이제 이
id
변수를 사용하여 항목 세부정보를 검색합니다.onViewCreated()
내id
를 전달하는viewModel
에서retrieveItem()
함수를 호출합니다.viewLifecycleOwner
와 람다를 전달하는 반환된 값에 관찰자를 연결합니다.
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) {
}
- 람다 내에서
selectedItem
을 데이터베이스에서 검색된Item
항목이 포함된 매개변수로 전달합니다. 람다 함수 본문에서selectedItem
값을item
에 할당합니다.item
을 전달하는bind()
함수를 호출합니다. 완성된 함수는 다음과 같습니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
item = selectedItem
bind(item)
}
}
- 앱을 실행합니다. Inventory 화면에서 목록 요소를 클릭하면 Item Details 화면이 표시됩니다. 화면이 더는 비어 있지 않으므로 이제 인벤토리 데이터베이스에서 검색된 항목 세부정보가 표시됩니다.
- Sell, Delete, FAB 버튼을 탭합니다. 아무 일도 일어나지 않습니다. 다음 작업에서는 이러한 버튼의 기능을 구현합니다.
5. 판매 항목 구현
이 작업에서는 앱 기능을 확장하여 판매 기능을 구현합니다. 다음은 이 단계의 안내가 요약되어 있습니다.
- ViewModel에 함수를 추가하여 항목을 업데이트합니다.
- 수량을 줄이고 앱 데이터베이스의 항목을 업데이트하는 새 메서드를 만듭니다.
- 클릭 리스너를 Sell 버튼에 연결합니다.
- 수량이 0이면 Sell 버튼을 사용 중지합니다.
이제 코드를 작성해보겠습니다.
InventoryViewModel
에서 항목 클래스Item
의 인스턴스를 사용하고 아무것도 반환하지 않는updateItem()
이라는 비공개 함수를 추가합니다.
private fun updateItem(item: Item) {
}
- 새 메서드
updateItem()
을 구현합니다.ItemDao
클래스에서update()
정지 메서드를 호출하려면viewModelScope
를 사용하여 코루틴을 시작합니다. launch 블록 내에서item
을 전달하는itemDao
의update()
함수를 호출합니다. 완성된 메서드는 다음과 같습니다.
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
- 계속
InventoryViewModel
내에서Item
항목 클래스의 인스턴스를 사용하고 아무것도 반환하지 않는sellItem()
이라는 또 다른 메서드를 추가합니다.
fun sellItem(item: Item) {
}
sellItem()
함수에서if
조건을 추가하여item.quantityInStock
이0
보다 큰지 확인합니다.
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
}
}
if
블록 내에서 데이터 클래스의 copy()
함수를 사용하여 항목을 업데이트합니다.
데이터 클래스: copy()
copy()
함수는 기본적으로 데이터 클래스의 모든 인스턴스에 제공됩니다. 이 함수는 일부 속성을 변경하지만 나머지 속성은 변경하지 않고 그대로 두기 위해 객체를 복사하는 데 사용됩니다.
예를 들어 다음과 같은 User
클래스와 인스턴스 jack
을 고려해보세요. age
속성만 업데이트하여 새 인스턴스를 만들려면 구현은 다음과 같습니다.
예
// Data class
data class User(val name: String = "", val age: Int = 0)
// Data class instance
val jack = User(name = "Jack", age = 1)
// A new instance is created with its age property changed, rest of the properties unchanged.
val olderJack = jack.copy(age = 2)
InventoryViewModel
의sellItem()
함수로 돌아갑니다.if
블록 내에서newItem
이라는 변경 불가능한 새 속성을 만듭니다. 업데이트된quantityInStock
(재고를1
씩 줄임)을 전달하는item
인스턴스에서copy()
함수를 호출합니다.
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
newItem
정의 아래에서 업데이트된 새 항목newItem
을 전달하는updateItem()
함수를 호출합니다. 완성된 메서드는 다음과 같습니다.
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
// Decrease the quantity by 1
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
- 재고 판매 기능을 추가하려면
ItemDetailFragment
로 이동합니다.bind()
함수 끝으로 스크롤합니다.apply
블록 내에서 Sell 버튼의 클릭 리스너를 설정하고viewModel
에서sellItem()
함수를 호출합니다.
private fun bind(item: Item) {
binding.apply {
...
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- 앱을 실행합니다. Inventory 화면에서 수량이 0보다 큰 목록 요소를 클릭합니다. Item Details 화면이 표시됩니다. Sell 버튼을 탭하고 수량 값이 1씩 감소하는지 확인합니다.
- Item Details 화면에서 Sell 버튼을 계속 탭하여 수량을 0으로 만듭니다. 도움말: 재고가 적은 항목을 선택하거나 수량이 적은 새 항목을 만드세요. 수량이 0이 되면 Sell 버튼을 탭합니다. 시각적인 변화는 없습니다. 이는
sellItem()
함수가 수량을 업데이트하기 전에 수량이 0보다 큰지 확인하기 때문입니다.
- 사용자에게 더 나은 피드백을 제공하려면 판매할 항목이 없을 때 Sell 버튼을 사용 중지하는 것이 좋습니다.
InventoryViewModel
에서 수량이0
보다 큰지 확인하는 함수를 추가합니다. 함수 이름을Item
인스턴스를 사용하고Boolean
을 반환하는isStockAvailable()
로 지정합니다.
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
ItemDetailFragment
로 이동하여bind()
함수로 스크롤합니다. apply 블록 내에서item
을 전달하는viewModel
에서isStockAvailable()
함수를 호출합니다. 반환 값을 Sell 버튼의isEnabled
속성으로 설정합니다. 코드는 다음과 같습니다.
private fun bind(item: Item) {
binding.apply {
...
sellItem.isEnabled = viewModel.isStockAvailable(item)
sellItem.setOnClickListener { viewModel.sellItem(item) }
}
}
- 앱을 실행하면 재고 수량이 0일 때 Sell 버튼이 사용 중지됩니다. 축하합니다. 앱에 항목 판매 기능을 구현했습니다.
항목 삭제
이전 작업과 마찬가지로 삭제 기능을 구현하여 앱 기능을 더 확장합니다. 다음은 이 단계의 안내가 요약되어 있습니다. 판매 기능을 구현하는 것보다 훨씬 쉽습니다.
- ViewModel에 함수를 추가하여 데이터베이스에서 항목을 삭제합니다.
ItemDetailFragment
에 새 메서드를 추가하여 새 삭제 함수를 호출하고 탐색을 처리합니다.- 클릭 리스너를 Delete 버튼에 연결합니다.
코드를 작성해보겠습니다.
InventoryViewModel
에서deleteItem()
이라는 새 함수를 추가합니다. 이 함수는item
이라는Item
항목 클래스의 인스턴스를 사용하고 아무것도 반환하지 않습니다.deleteItem()
함수 내에서viewModelScope
를 사용하여 코루틴을 시작합니다.launch
블록 내에서item
을 전달하는itemDao
에서delete()
메서드를 호출합니다.
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
ItemDetailFragment
에서deleteItem()
함수의 시작 부분으로 스크롤합니다.viewModel
에서deleteItem()
을 호출하고item
을 전달합니다.item
인스턴스에는 Item Details 화면에 현재 표시된 항목이 포함되어 있습니다. 완성된 메서드는 다음과 같습니다.
private fun deleteItem() {
viewModel.deleteItem(item)
findNavController().navigateUp()
}
- 계속
ItemDetailFragment
내에서showConfirmationDialog()
함수로 스크롤합니다. 이 함수는 시작 코드의 일부로 제공됩니다. 이 메서드는 항목을 삭제하기 전에 사용자의 확인을 받는 알림 대화상자를 표시하고 긍정적인 버튼을 탭하면deleteItem()
함수를 호출합니다.
private fun showConfirmationDialog() {
MaterialAlertDialogBuilder(requireContext())
...
.setPositiveButton(getString(R.string.yes)) { _, _ ->
deleteItem()
}
.show()
}
showConfirmationDialog()
함수는 다음과 같은 알림 대화상자를 표시합니다.
ItemDetailFragment
에서bind()
함수 끝apply
블록 내에 삭제 버튼의 클릭 리스너를 설정합니다. 클릭 리스너 람다 내에서showConfirmationDialog()
를 호출합니다.
private fun bind(item: Item) {
binding.apply {
...
deleteItem.setOnClickListener { showConfirmationDialog() }
}
}
- 앱을 실행합니다. Inventory 목록 화면에서 목록 요소를 선택하고 Item Details 화면에서 Delete 버튼을 탭합니다. Yes를 탭하면 앱이 Inventory 화면으로 돌아갑니다. 삭제한 항목은 더 이상 앱 데이터베이스에 없습니다. 축하합니다. 삭제 기능을 구현했습니다.
항목 수정
이전 작업과 마찬가지로 이 작업에서는 또 다른 개선된 기능을 앱에 추가합니다. 항목 수정 기능을 구현합니다.
다음은 앱 데이터베이스에서 항목을 수정하는 이 단계를 간략히 요약한 것입니다.
- 프래그먼트 제목을 Edit Item으로 업데이트하여 Add Item 화면을 재사용합니다.
- 클릭 리스너를 FAB에 추가하여 Edit Item 화면으로 이동합니다.
- 항목 세부정보로 TextView를 채웁니다.
- Room을 사용하여 데이터베이스의 항목을 업데이트합니다.
FAB에 클릭 리스너 추가
ItemDetailFragment
에서 매개변수를 사용하지 않고 아무것도 반환하지 않는editItem()
이라는 새private
함수를 추가합니다. 다음 단계에서는 화면 제목을 Edit Item으로 업데이트하여fragment_add_item.xml
을 재사용합니다. 프래그먼트 제목 문자열을 항목 id와 함께 작업의 일부로 전송하면 됩니다.
private fun editItem() {
}
프래그먼트 제목을 업데이트하면 Edit Item 화면이 다음과 같이 표시됩니다.
editItem()
함수 내에서action
이라는 변경 불가능한 변수를 만듭니다. 제목 문자열edit_fragment_title
과 항목id
를 전달하는ItemDetailFragmentDirections
에서actionItemDetailFragmentToAddItemFragment()
를 호출합니다. 반환된 값을action
에 할당합니다.action
정의 아래에서action
을 전달하는this.findNavController().navigate()
를 호출하여 Edit Item 화면으로 이동합니다.
private fun editItem() {
val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
getString(R.string.edit_fragment_title),
item.id
)
this.findNavController().navigate(action)
}
ItemDetailFragment
내에서bind()
함수로 스크롤합니다.apply
블록 내에서 FAB의 클릭 리스너를 설정하고 람다에서editItem()
함수를 호출하여 Edit Item 화면으로 이동합니다.
private fun bind(item: Item) {
binding.apply {
...
editItem.setOnClickListener { editItem() }
}
}
- 앱을 실행합니다. Item Details 화면으로 이동합니다. FAB를 클릭합니다. 화면 제목이 Edit Item으로 업데이트되었지만 모든 텍스트 필드가 비어 있습니다. 다음 단계에서 이 문제를 수정합니다.
TextView 채우기
이 단계에서는 Edit Item 화면의 텍스트 필드를 항목 세부정보로 채웁니다. Add Item
화면을 사용하고 있으므로 새 함수를 Kotlin 파일 AddItemFragment.kt
에 추가합니다.
AddItemFragment
에서 텍스트 필드를 항목 세부정보와 바인딩하는 새private
함수를 추가합니다. 함수 이름을 항목 클래스의 인스턴스를 가져와 아무것도 반환하지 않는bind()
로 지정합니다.
private fun bind(item: Item) {
}
bind()
함수 구현은 앞서ItemDetailFragment
에서 한 작업과 매우 비슷합니다.bind()
함수 내에서format()
함수를 사용하여 가격을 소수점 이하 두 자리로 반올림한 후 다음과 같이price
라는val
에 할당합니다.
val price = "%.2f".format(item.itemPrice)
price
정의 아래에서 다음과 같이binding
속성에apply
범위 함수를 사용합니다.
binding.apply {
}
apply
범위 함수 코드 블록 내에서item.itemName
을itemName
의 텍스트 속성으로 설정합니다.setText()
함수를 사용하고item.itemName
문자열과TextView.BufferType.SPANNABLE
을BufferType
으로 전달합니다.
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
}
Android 스튜디오에서 메시지가 표시되면 android.widget.TextView
를 가져옵니다.
- 위 단계와 마찬가지로 가격
EditText
의 텍스트 속성을 다음과 같이 설정합니다. 수량 EditText의text
속성을 설정하려면item.quantityInStock
을String
으로 변환해야 합니다. 완성된 함수는 다음과 같습니다.
private fun bind(item: Item) {
val price = "%.2f".format(item.itemPrice)
binding.apply {
itemName.setText(item.itemName, TextView.BufferType.SPANNABLE)
itemPrice.setText(price, TextView.BufferType.SPANNABLE)
itemCount.setText(item.quantityInStock.toString(), TextView.BufferType.SPANNABLE)
}
}
AddItemFragment
내에서onViewCreated()
함수로 스크롤합니다. 슈퍼 클래스 함수 호출 후id
라는val
을 만들고 탐색 인수에서itemId
를 검색합니다.
val id = navigationArgs.itemId
id
가 0보다 큰지 확인하는 조건으로if-else
블록을 추가하고 Save 버튼 클릭 리스너를else
블록으로 이동합니다.if
블록 내에서id
를 사용하여 항목을 검색하고 관찰자를 추가합니다. 관찰자 내에서item
속성을 업데이트하고item
을 전달하는bind()
를 호출합니다. 복사하여 붙여넣을 수 있도록 완성된 함수가 제공됩니다. 간단하고 이해하기 쉬우므로 직접 파악할 수 있습니다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
if (id > 0) {
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
item = selectedItem
bind(item)
}
} else {
binding.saveAction.setOnClickListener {
addNewItem()
}
}
}
- 앱을 실행하여 Item Details로 이동한 다음 + FAB를 탭합니다. 필드가 항목 세부정보로 채워져 있습니다. 재고 수량이나 다른 필드를 수정하고 저장 버튼을 탭합니다. 아무 일도 일어나지 않습니다. 앱 데이터베이스의 항목을 업데이트하지 않기 때문입니다. 이 문제는 곧 수정합니다.
Room을 사용하여 항목 업데이트
이 마지막 작업에서는 최종 코드를 추가하여 업데이트 기능을 구현합니다. ViewModel에서 필요한 함수를 정의하고 이를 AddItemFragment
에서 사용합니다.
이제 코드를 작성해보겠습니다.
InventoryViewModel
에서Int
와 항목 세부정보 문자열itemName
,itemPrice
,itemCount
세 개를 가져오는getUpdatedItemEntry()
라는private
함수를 추가합니다. 함수에서Item
의 인스턴스를 반환합니다. 참조용으로 코드가 제공됩니다.
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
}
getUpdatedItemEntry()
함수 내에서 다음과 같이 함수 매개변수를 사용하여 Item 인스턴스를 만듭니다. 함수에서Item
인스턴스를 반환합니다.
private fun getUpdatedItemEntry(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
): Item {
return Item(
id = itemId,
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
InventoryViewModel
내에서updateItem()
이라는 또 다른 함수를 추가합니다. 이 함수도Int
와 항목 세부정보 문자열 세 개를 사용하고 아무것도 반환하지 않습니다. 다음 코드 스니펫에서 변수 이름을 사용합니다.
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
}
updateItem()
함수 내에서 다음과 같이 항목 정보(함수 매개변수로 전달됨)를 전달하는getUpdatedItemEntry()
함수를 호출합니다. 반환된 값을updatedItem
이라는 변경 불가능한 변수에 할당합니다.
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
getUpdatedItemEntry()
함수 호출 바로 아래에서updatedItem
을 전달하는updateItem()
함수를 호출합니다. 완성된 함수는 다음과 같습니다.
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
updateItem(updatedItem)
}
AddItemFragment
로 돌아가서 매개변수 없이updateItem()
이라는 비공개 함수를 추가하고 아무것도 반환하지 않습니다. 함수 내에서isEntryValid()
함수를 호출하여 사용자 입력을 검증하는if
조건을 추가합니다.
private fun updateItem() {
if (isEntryValid()) {
}
}
if
블록 내에서 항목 세부정보를 전달하는viewModel.updateItem()
을 호출합니다. 탐색 인수의itemId
와 EditText의 이름, 가격, 수량과 같은 기타 항목 세부정보를 다음과 같이 사용합니다.
viewModel.updateItem(
this.navigationArgs.itemId,
this.binding.itemName.text.toString(),
this.binding.itemPrice.text.toString(),
this.binding.itemCount.text.toString()
)
updateItem()
함수 호출 아래에서action
이라는val
을 정의합니다.AddItemFragmentDirections
에서actionAddItemFragmentToItemListFragment()
를 호출하고 반환된 값을action
에 할당합니다.ItemListFragment
로 이동하여action
을 전달하는findNavController().navigate()
를 호출합니다.
private fun updateItem() {
if (isEntryValid()) {
viewModel.updateItem(
this.navigationArgs.itemId,
this.binding.itemName.text.toString(),
this.binding.itemPrice.text.toString(),
this.binding.itemCount.text.toString()
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
AddItemFragment
내에서bind()
함수로 스크롤합니다.binding.
apply
범위 함수 블록 내에서 Save 버튼의 클릭 리스너를 설정합니다. 다음과 같이 람다 내에서updateItem()
함수를 호출합니다.
private fun bind(item: Item) {
...
binding.apply {
...
saveAction.setOnClickListener { updateItem() }
}
}
- 앱을 실행합니다. 인벤토리 항목을 수정해보세요. Inventory 앱 데이터베이스의 모든 항목을 수정할 수 있습니다.
축하합니다. Room을 사용하여 앱 데이터베이스를 관리하는 첫 번째 앱을 만들었습니다.
6. 솔루션 코드
이 Codelab의 솔루션 코드는 아래와 같이 GitHub 저장소와 분기에 있습니다.
이 Codelab의 코드를 가져와서 Android 스튜디오에서 열려면 다음을 실행합니다.
코드 가져오기
- 제공된 URL을 클릭합니다. 브라우저에서 프로젝트의 GitHub 페이지가 열립니다.
- 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 대화상자를 엽니다.
- 대화상자에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
- 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
- ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.
Android 스튜디오에서 프로젝트 열기
- Android 스튜디오를 시작합니다.
- Welcome to Android Studio 창에서 Open an existing Android Studio project를 클릭합니다.
참고: Android 스튜디오가 이미 열려 있는 경우 File > New > Import Project 메뉴 옵션을 대신 선택합니다.
- Import Project 대화상자에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
- 프로젝트 폴더를 더블클릭합니다.
- Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
- Run 버튼 을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.
- Project 도구 창에서 프로젝트 파일을 살펴보고 앱이 구현된 방식을 확인합니다.
7. 요약
- Kotlin은 클래스에서 상속받거나 기존 클래스 정의를 수정하지 않고도 새로운 기능으로 클래스를 확장하는 기능을 제공합니다. 확장이라는 특수 선언을 통해 실행됩니다.
Flow
데이터를LiveData
값으로 사용하려면asLiveData()
함수를 사용합니다.copy()
함수는 기본적으로 데이터 클래스의 모든 인스턴스에 제공됩니다. 이를 통해 객체를 복사하여 일부 속성은 변경하면서 나머지 속성은 변경하지 않고 그대로 유지할 수 있습니다.
8. 자세히 알아보기
Android 개발자 문서
- 대상 간 데이터 전달
- Android String
- Android Formatter
- Database Inspector로 데이터베이스 디버그
- Room을 사용하여 로컬 데이터베이스에 데이터 저장
API 참조
Kotlin 참조