이전 Codelab에서 Room을 사용해 앱 데이터를 저장하는 과정까지 진행했다. 이제 데이터베이스의 데이터를 읽고, 표시하고, 업데이트하고, 삭제하는 방법을 알아보자.
Kotlin은 클래스에서 상속받거나 기존 클래스 정의를 수정하지 않고도 새로운 기능으로 클래스를 확장하는 기능을 제공한다.
Item
클래스에서 확장 함수를 이용해 string을 통화 형식 문자열로 바꿔보자.
Item.kt
에 ITem.getFormattedPrice()
라는 확장 함수를 추가한다.
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
)
)
)
}
ViewHolder
를 반환한다.ItemListItemBinding
을 사용해 item_list_item.xml
레이아웃에서 확장한다. override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val current = getItem(position)
holder.itemView.setOnClickListener {
onItemClicked(current)
}
holder.bind(current)
}
getItem()
으로 현재 item을 가져와 위치를 전달한다. class ItemViewHolder(private var binding: ItemListItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
}
}
companion object를 정의한다.
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
}
}
}
4번에서 작성한 bind()
를 구현한다.
fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemQuantity.text = item.quantityInStock.toString()
}
}
itemPrice
는 앞서 만들어둔 확장 함수를 사용해 통화 형식의 가격을 바인딩한다.ListAdapter를 사용하려면 InventoryViewModel과 ItemListFragment를 수정해야 한다.
InventoryViewModel
클래스 시작 부분에 데이터베이스의 item에 관한 LiveData<List<Item>>
유형의 allItems
변수를 작성한다.
val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()
getITems()
를 호출해 반환된 Flow
를 asLiveData()
로 LiveData
로 바꿔 변수에 할당한다.
클래스 시작 부분에 InventoryViewModel
유형의 viewModel
을 선언한다. InventoryViewModelFactory
생성자를 전달합니다.
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
이제 onViewCreated()
내에 코드를 추가한다.
val adapter = ItemListAdapter{
}
binding.recyclerView.adapter = adapter
viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
items.let {
adapter.submitList(it)
}
}
adapter
를 선언하고, recyclerView에 바인딩한다.allItems
에 관찰자를 연결한다.adapter
에서 submitList()
를 호출해 새 목록을 전달한다. 이를통해 RecyclerView가 업데이트된다.item을 클릭했을 때 세부정보를 표시하는 페이지를 만들어보자.
val adapter = ItemListAdapter {
val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
this.findNavController().navigate(action)
}
onViewCreated()
내부에 있는 adapter
에 action을 정의한다.findNavComtroller
와 navigate
로 액션을 연결한다. InventoryViewModel
에서 id를 받아 LiveData<Item>
을 반환하는 retrievItem()
메서드를 정의한다.
fun retrieveItem(id: Int): LiveData<Item> {
return itemDao.getItem(id).asLiveData()
}
ItemDetailFragment
에서 ViewModel
인스턴스를 만들고 ViewModel
데이터를 Item Details 화면의 TextView
에 바인딩한다.
viewModel
변수를 정의한다. private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
bind()
함수를 만든다. private fun bind(item: Item){
binding.apply {
binding.itemName.text = item.itemName
binding.itemPrice.text = item.getFormattedPrice()
binding.itemCount.text = item.quantityInStock.toString()
}
}
onViewCreated()
를 생성한다. 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)
}
}
id
변수에 할당한다.id
와 retrieveItem()
을 사용해 item detail을 검색한다.selectedItem
을 데이터베이스에서 검색된 Item
항목이 포함된 매개변수로 전달. selectedItem
값을 item
에 할당. item
을 전달해 bind()
함수를 호출한다.이제 화면에 Inventory에 있는 아이템 목록이 나타나고, 목록에 추가할 수 있다. 아이템을 클릭하면 상세 페이지를 확인할 수 있다.
InventoryViewModel
에 updateItem()
메서드를 구현한다.
private fun updateItem(item: Item){
viewModelScope.launch {
itemDao.update(item)
}
}
ItemDao
클래스에서 update()
supend 메서드를 호출하려면 viewModelScope
를 사용해 코루틴을 시작해야한다. fun sellItem(item: Item){
if(item.quantityInStock > 0){
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
copy()
를 사용해 수량을 하나 줄인고, updateItem()
으로 업데이트 한다.copy()
함수는 기본적으로 데이터 클래스의 모든 인스턴스에 제공된다.
copy()
는 일부 속성은 변경하고 나머지는 그대로 복사하는 함수.
즉 위 코드에서 quantityInStock
값만 -1한 값이 들어가고, 나머지는 그대로 복사해서 newItem
에 담는 것이다.
ItemDetailFragment
의 bind()
함수에 setOnClickListener를 추가한다.
sellItem.setOnClickListener {
viewModel.sellItem(item)
}
InventoryViewModel
에 isStockAvailable()
함수를 추가한다.
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
ItemDetailFragment
의 bind()
함수에 sellItem.isEnabled
속성을 설정한다.
sellItem.isEnabled = viewModel.isStockAvailable(item)
삭제 과정도 update와 유사하다. (더 간단)
InventoryViewModel
에서 deleteItem()
이라는 새 함수를 추가한다.
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
여기에 viewModel.deleteItem(item)
을 추가한다.
ItemDetailFragment
에는 showConfirmationDialog()
라는 함수가 있다. 삭제 전 사용자에게 한 번 더 확인을 받는 dialog인데, 삭제 버튼을 눌렀을 때 이 함수를 실행해야 한다.
ItemDetailFragment
에서 bind()
함수에
deleteItem.setOnClickListener { showConfirmationDialog() }
를 추가해 버튼을 누르면 dialog가 뜨도록 한다.
ItemDetailFragment
Edit Item 화면은 Add Item에 사용했던 화면을 재사용한다.
private fun editItem() {
val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
getString(R.string.edit_fragment_title),
item.id
)
this.findNavController().navigate(action)
}
fragment_add_item.xml
로 연결되는 action을 할당한다. editItem.setOnClickListener { editItem() }
ItemDetailFragment
의 bind()
함수에 이 코드를 추가해 FAB 버튼을 눌렀을 때 editItem()
함수를 호출하도록 설정한다. 현재는 화면은 완성했지만 텍스트필드가 빈 화면이다. 저장되어 있는 값을 가져와 텍스트 필드를 채워보자.
AddITemFragment.kt
에서 바인딩을 해야한다. bind()
함수를 추가한다.SPANNABLE
로 한다. 🤔? 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()
에서 bind()
를 호출한다.
val id = navigationArgs.itemId
if (id > 0) {
viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
item = selectedItem
bind(item)
}
} else {
binding.saveAction.setOnClickListener {
addNewItem()
}
}
retreiveItem()
으로 item을 가져와 바인드한다.addNewItem()
이 실행된다. 이제 수정 FAB을 클릭했을 때 텍스트 필드가 채워진 화면이 뜬다. 하지만 아직 값 수정 후 저장하는 기능이 없다.
InventoryViewModel
에 새로 입력된 값으로 업데이트하는 함수를 구현한다.
private fun getUpdatedItemEntry( itemId: Int, itemName: String, itemPrice: String, itemCount: String ): Item{
return Item(
id = itemId,
itemName = itemName,
itemPrice = itemPrice.toDouble(),
quantityInStock = itemCount.toInt()
)
}
fun updateItem( itemId: Int, itemName: String, itemPrice: String, itemCount: String ){
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
updateItem(updatedItem)
}
그 다음 AddItemFragment
에 updateItem()
함수를 추가한다.
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()
함수에 클릭 리스너를 설정한다.
saveAction.setOnClickListener{ updateItem() }