Unit 5: Data persistence (3)

quokka·2021년 12월 25일
0

Android Basics in Kotlin

목록 보기
23/25
post-thumbnail

이전 Codelab에서 Room을 사용해 앱 데이터를 저장하는 과정까지 진행했다. 이제 데이터베이스의 데이터를 읽고, 표시하고, 업데이트하고, 삭제하는 방법을 알아보자.

확장 함수

Kotlin은 클래스에서 상속받거나 기존 클래스 정의를 수정하지 않고도 새로운 기능으로 클래스를 확장하는 기능을 제공한다.

Item 클래스에서 확장 함수를 이용해 string을 통화 형식 문자열로 바꿔보자.

Item.ktITem.getFormattedPrice()라는 확장 함수를 추가한다.

ListAdapter

1. ItemListAdapter 파일 생성

class ItemListAdapter(private val onItemClicked: (Item) -> Unit) :
   ListAdapter<Item, ItemListAdapter.ItemViewHolder>(DiffCallback) {
   }

2. onCreateViewHolder()

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
       return ItemViewHolder(
           ItemListItemBinding.inflate(
               LayoutInflater.from(
                   parent.context
               )
           )
       )
   }
  • onCreateViewHolder는 RecyclerView에게 새 ViewHolder를 반환한다.
  • 새 View를 만들고 자동 생성된 바인딩 클래스 ItemListItemBinding을 사용해 item_list_item.xml 레이아웃에서 확장한다.

3. onBindViewHolder()

   override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
       val current = getItem(position)
       holder.itemView.setOnClickListener {
           onItemClicked(current)
       }
       holder.bind(current)
   }
  • getItem()으로 현재 item을 가져와 위치를 전달한다.

4. ItemViewHolder

   class ItemViewHolder(private var binding: ItemListItemBinding) :
       RecyclerView.ViewHolder(binding.root) {

       fun bind(item: Item) {

       }
   }
  • ItemViewHolder 클래스를 정의하고, RecyclerView.ViewHolder.에서 확장한다.
  • bind() 함수 재정의

5. companion object

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
           }
       }
   }

6. bind 함수 구현

4번에서 작성한 bind()를 구현한다.

        fun bind(item: Item) {
            binding.apply {
                itemName.text = item.itemName
                itemPrice.text = item.getFormattedPrice()
                itemQuantity.text = item.quantityInStock.toString()
            }
        }
  • itemPrice는 앞서 만들어둔 확장 함수를 사용해 통화 형식의 가격을 바인딩한다.

ListAdapter 사용하도록 수정

ListAdapter를 사용하려면 InventoryViewModel과 ItemListFragment를 수정해야 한다.

1. InventoryViewModel

InventoryViewModel 클래스 시작 부분에 데이터베이스의 item에 관한 LiveData<List<Item>> 유형의 allItems 변수를 작성한다.

val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData()

getITems()를 호출해 반환된 FlowasLiveData()LiveData로 바꿔 변수에 할당한다.

2. ItemListFragment

클래스 시작 부분에 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 Details

item을 클릭했을 때 세부정보를 표시하는 페이지를 만들어보자.

1. 클릭 핸들러

val adapter = ItemListAdapter {
   val action =   ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
   this.findNavController().navigate(action)
}
  • onViewCreated() 내부에 있는 adapter에 action을 정의한다.
  • action을 정의하고, findNavComtrollernavigate로 액션을 연결한다.
  • 이제 item을 클릭했을 때 Item Detail 화면으로 이동된다.

2. Item detail 가져오기

InventoryViewModel에서 id를 받아 LiveData<Item>을 반환하는 retrievItem() 메서드를 정의한다.

fun retrieveItem(id: Int): LiveData<Item> {
   return itemDao.getItem(id).asLiveData()
}

3. TextView에 데이터 바인딩

ItemDetailFragment에서 ViewModel 인스턴스를 만들고 ViewModel 데이터를 Item Details 화면의 TextView에 바인딩한다.

  • lateinit으로 item 속성을 추가한다.
  • 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 변수에 할당한다.
  • idretrieveItem()을 사용해 item detail을 검색한다.
  • 람다 내에서 selectedItem을 데이터베이스에서 검색된 Item 항목이 포함된 매개변수로 전달. selectedItem 값을 item에 할당. item을 전달해 bind() 함수를 호출한다.

이제 화면에 Inventory에 있는 아이템 목록이 나타나고, 목록에 추가할 수 있다. 아이템을 클릭하면 상세 페이지를 확인할 수 있다.

판매 기능 (update 구현)

1. updateItem()

InventoryViewModelupdateItem() 메서드를 구현한다.

    private fun updateItem(item: Item){
        viewModelScope.launch { 
            itemDao.update(item)
        }
    }
  • ItemDao 클래스에서 update() supend 메서드를 호출하려면 viewModelScope를 사용해 코루틴을 시작해야한다.

2. sellItem()

    fun sellItem(item: Item){
        if(item.quantityInStock > 0){
            val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
            updateItem(newItem)
        }
    }
  • 수량이 남아있다면 copy()를 사용해 수량을 하나 줄인고, updateItem()으로 업데이트 한다.

copy()

copy() 함수는 기본적으로 데이터 클래스의 모든 인스턴스에 제공된다.
copy()는 일부 속성은 변경하고 나머지는 그대로 복사하는 함수.

즉 위 코드에서 quantityInStock 값만 -1한 값이 들어가고, 나머지는 그대로 복사해서 newItem에 담는 것이다.

3. setOnClickListener

ItemDetailFragmentbind() 함수에 setOnClickListener를 추가한다.

    sellItem.setOnClickListener { 
    	viewModel.sellItem(item) 
    }

4. 재고가 없으면 Sell 버튼 비활성화

InventoryViewModelisStockAvailable() 함수를 추가한다.

fun isStockAvailable(item: Item): Boolean {
   return (item.quantityInStock > 0)
}

ItemDetailFragmentbind() 함수에 sellItem.isEnabled 속성을 설정한다.

sellItem.isEnabled = viewModel.isStockAvailable(item)

삭제 기능 (delete)

삭제 과정도 update와 유사하다. (더 간단)

1. InventoryViewModel deleteItem()

InventoryViewModel에서 deleteItem()이라는 새 함수를 추가한다.

fun deleteItem(item: Item) {
   viewModelScope.launch {
       itemDao.delete(item)
   }
}

2. ItemDetailFragment deleteItem()

여기에 viewModel.deleteItem(item)을 추가한다.

3. showConfirmationDialog()

ItemDetailFragment에는 showConfirmationDialog()라는 함수가 있다. 삭제 전 사용자에게 한 번 더 확인을 받는 dialog인데, 삭제 버튼을 눌렀을 때 이 함수를 실행해야 한다.

ItemDetailFragment에서 bind() 함수에
deleteItem.setOnClickListener { showConfirmationDialog() }를 추가해 버튼을 누르면 dialog가 뜨도록 한다.

Edit 기능

1. Edit Item 화면

ItemDetailFragment Edit Item 화면은 Add Item에 사용했던 화면을 재사용한다.

private fun editItem() {
   val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
       getString(R.string.edit_fragment_title),
       item.id
   )
   this.findNavController().navigate(action)
}
  • action에 add item에 사용했던 화면인 fragment_add_item.xml로 연결되는 action을 할당한다.
  • 매개변수로 "Edit Item" 문자열과 id를 전달한다.
editItem.setOnClickListener { editItem() }
  • ItemDetailFragmentbind() 함수에 이 코드를 추가해 FAB 버튼을 눌렀을 때 editItem() 함수를 호출하도록 설정한다.

2. Edit Item 화면에 텍스트 채우기

현재는 화면은 완성했지만 텍스트필드가 빈 화면이다. 저장되어 있는 값을 가져와 텍스트 필드를 채워보자.

  • Add Item 화면을 사용하고 있으므로 AddITemFragment.kt에서 바인딩을 해야한다.
  • 다음과 같이 bind() 함수를 추가한다.
  • 문자열의 BufferType은 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)
        }
    }

AddItemFragmentonViewCreated()에서 bind()를 호출한다.

val id = navigationArgs.itemId
   if (id > 0) {
       viewModel.retrieveItem(id).observe(this.viewLifecycleOwner) { selectedItem ->
           item = selectedItem
           bind(item)
       }
   } else {
       binding.saveAction.setOnClickListener {
           addNewItem()
       }
   }
  • 매개변수로 받은 id를 id 변수에 저장하고, id가 0보다 큰지 확인한다.
  • 0보다 크다면 retreiveItem()으로 item을 가져와 바인드한다.
  • 아니라면 원래 SAVE 버튼을 눌렀을 때 실행되었던 addNewItem()이 실행된다.

이제 수정 FAB을 클릭했을 때 텍스트 필드가 채워진 화면이 뜬다. 하지만 아직 값 수정 후 저장하는 기능이 없다.

3. 업데이트

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)
    }

그 다음 AddItemFragmentupdateItem() 함수를 추가한다.

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)
   }
}

마지막으로 AddITemFragmentbind() 함수에 클릭 리스너를 설정한다.

saveAction.setOnClickListener{ updateItem() }

0개의 댓글