관리 메뉴

나만을 위한 블로그

[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 3 - 본문

Android

[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 3 -

참깨빵위에참깨빵 2022. 12. 13. 21:46
728x90
반응형

지금까지 Room DB 기본 설정을 끝내고 데이터를 추가하는 로직까지 추가했다.

이제 전체 아이템 조회 함수를 통해 DB에 저장된 모든 데이터를 리사이클러뷰에 붙이는 처리를 구현한다. 데이터가 변경되면 바로 알 수 있도록 ListAdapter와 diffcallback을 사용한다.

이전 글들의 링크는 아래에 있다.

 

https://onlyfor-me-blog.tistory.com/556

 

[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 1 -

예전에 자바로 같은 내용의 포스팅을 쓴 적이 있다. https://onlyfor-me-blog.tistory.com/290 [Android] Room DB 사용법 21.04.13 - 코드 테스트 후 정상 작동 확인, xml 코드 별 어떤 xml의 코드인지 기재 이번 포스팅

onlyfor-me-blog.tistory.com

https://onlyfor-me-blog.tistory.com/558

 

[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 2 -

이전 글에서 Room을 사용하기 위한 기본 준비들을 모두 끝냈으니 이제 Room을 사용하기 위한 뷰모델을 만들고 프래그먼트에 실제로 사용한다. 이전 글은 아래 링크를 타고 이동하면 볼 수 있다. htt

onlyfor-me-blog.tistory.com

 

먼저 Item의 가격을 달러에 맞춰 $22.00 형태로 표시하는 확장 함수를 Item 클래스에 추가한다.

 

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.text.NumberFormat

@Entity(tableName = "item")
data class Item(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,
    @ColumnInfo(name = "name")
    val itemName: String,
    @ColumnInfo(name = "price")
    val itemPrice: Double,
    @ColumnInfo(name = "quantity")
    val quantityInStock: Int
)

fun Item.getFormattedPrice(): String = NumberFormat.getCurrencyInstance().format(itemPrice)

 

그리고 ItemListAdapter 클래스를 만들고 아래와 같이 작성한다.

 

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.kotlinprac.databinding.ItemListItemBinding
import com.example.kotlinprac.room.data.Item
import com.example.kotlinprac.room.data.getFormattedPrice

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

    class ItemViewHolder(private var binding: ItemListItemBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(item: Item) {
            binding.apply {
                itemName.text = item.itemName
                itemPrice.text = item.getFormattedPrice()
                itemQuantity.text = item.quantityInStock.toString()
            }
        }
    }

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

    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가지 정도 있는데 그 중 하나를 사용하고 있다.

궁금하다면 아래 포스팅을 참고하면 된다.

 

https://onlyfor-me-blog.tistory.com/525

 

[Android] 코틀린으로 리사이클러뷰 클릭 이벤트 구현하는 방법 정리

리사이클러뷰 클릭 이벤트에 대해선 예전에 자바로 작성한 적이 있다. https://onlyfor-me-blog.tistory.com/40 [Android] 리사이클러뷰 클릭 이벤트 2 참고한 사이트 : https://recipes4dev.tistory.com/168 리사이클러

onlyfor-me-blog.tistory.com

 

ItemListAdapter를 만들었다면 이제 사용할 차례다. 먼저 InventoryViewModel에 DB 데이터들을 LiveData 형태로 저장할 프로퍼티를 만들고, 해당 프로퍼티에 getItems()의 결과를 담는다.

 

class InventoryViewModel(
    private val itemDao: ItemDao
): ViewModel() {
    val allItems: LiveData<List<Item>> = itemDao.getItems().asLiveData() // 이 줄을 추가

    private fun insertItem(item: Item) {
        viewModelScope.launch {
            itemDao.insert(item)
        }
    }

    private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item =
        Item(
            itemName = itemName,
            itemPrice = itemPrice.toDouble(),
            quantityInStock = itemCount.toInt()
        )
    
    fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
        val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
        insertItem(newItem)
    }
    
    fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
        if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
            return false
        }
        return true
    }
}

class InventoryViewModelFactory(
    private val itemDao: ItemDao
): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        require(modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
            throw IllegalArgumentException("Unknown ViewModel class")
        }

        @Suppress("UNCHECKED_CAST")
        return InventoryViewModel(itemDao) as T
    }
}

 

프래그먼트에서 저 값의 변경을 관찰하기 위해 asLiveData()를 써서 Flow를 LiveData로 변환한다.

그리고 ItemListFragment로 이동해서 아래 코드를 작성한다.

 

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.ItemListFragmentBinding

class ItemListFragment : Fragment() {

    private var _binding: ItemListFragmentBinding? = null
    private val binding
        get() = _binding!!

    private val viewModel: InventoryViewModel by activityViewModels {
        InventoryViewModelFactory(
            (activity?.application as InventoryApplication).database.itemDao()
        )
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = ItemListFragmentBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val adapter = ItemListAdapter {
            val action = ItemListFragmentDirections.actionItemListFragmentToItemDetailFragment(it.id)
            this.findNavController().navigate(action)
        }
        binding.recyclerView.adapter = adapter

        viewModel.allItems.observe(this.viewLifecycleOwner) { items ->
            items.let {
               adapter.submitList(it)
            }
        }

        binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
        binding.floatingActionButton.setOnClickListener {
            val action = ItemListFragmentDirections.actionItemListFragmentToAddItemFragment(
                getString(R.string.add_fragment_title)
            )
            this.findNavController().navigate(action)
        }
    }

}

 

ItemListAdapter {} 안에서 클릭 시 네비게이션을 사용해서 화면을 이동시키는 게 보인다.

이렇게 하고 앱을 실행하면 상세 화면으로 이동하고 sell, delete 버튼이 각각 보인다. 아직 화면을 이동시켜서 표시하는 처리만 구현했기 때문에 그 외에 별다른 건 안 보인다.

이제 넘겨받은 id를 통해 특정 아이템의 정보들을 가져오는 처리를 구현한다. InventoryViewModel에 retrieveItem()을 만든다.

 

class InventoryViewModel(
    private val itemDao: ItemDao
): ViewModel() {

    private fun insertItem(item: Item) {
        viewModelScope.launch {
            itemDao.insert(item)
        }
    }

    private fun getNewItemEntry(itemName: String, itemPrice: String, itemCount: String): Item =
        Item(
            itemName = itemName,
            itemPrice = itemPrice.toDouble(),
            quantityInStock = itemCount.toInt()
        )
    
    fun addNewItem(itemName: String, itemPrice: String, itemCount: String) {
        val newItem = getNewItemEntry(itemName, itemPrice, itemCount)
        insertItem(newItem)
    }
    
    fun isEntryValid(itemName: String, itemPrice: String, itemCount: String): Boolean {
        if (itemName.isBlank() || itemPrice.isBlank() || itemCount.isBlank()) {
            return false
        }
        return true
    }
    
    fun retrieveItem(id: Int): LiveData<Item> {
        return itemDao.getItem(id).asLiveData()
    }
}

class InventoryViewModelFactory(
    private val itemDao: ItemDao
): ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        require(modelClass.isAssignableFrom(InventoryViewModel::class.java)) {
            throw IllegalArgumentException("Unknown ViewModel class")
        }

        @Suppress("UNCHECKED_CAST")
        return InventoryViewModel(itemDao) as T
    }
}

 

retrieveItem() 안에서도 마찬가지로 Flow를 LiveData로 사용하기 위해 asLiveData()를 사용한 게 보인다.

이제 상세 화면의 텍스트뷰에 가져온 값들을 알맞게 표시해야 한다. ItemDetailFragment 파일을 아래와 같이 작성한다.

 

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.example.kotlinprac.R
import com.example.kotlinprac.databinding.FragmentItemDetailBinding
import com.example.kotlinprac.room.data.Item
import com.example.kotlinprac.room.data.getFormattedPrice
import com.google.android.material.dialog.MaterialAlertDialogBuilder

class ItemDetailFragment : Fragment() {

    private var _binding: FragmentItemDetailBinding? = null
    private val binding get() = _binding!!
    private val navigationArgs: ItemDetailFragmentArgs by navArgs()

    lateinit var item: Item
    private val viewModel: InventoryViewModel by activityViewModels {
        InventoryViewModelFactory(
            (activity?.application as InventoryApplication).database.itemDao()
        )
    }

    private fun bind(item: Item) {
        binding.apply {
            itemName.text = item.itemName
            itemPrice.text = item.getFormattedPrice()
            itemCount.text = item.quantityInStock.toString()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentItemDetailBinding.inflate(inflater, container, false)
        return binding.root
    }

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

    private fun showConfirmationDialog() {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle(getString(android.R.string.dialog_alert_title))
            .setMessage(getString(R.string.delete_question))
            .setCancelable(false)
            .setNegativeButton(getString(R.string.no)) { _, _ -> }
            .setPositiveButton(getString(R.string.yes)) { _, _ ->
                deleteItem()
            }
            .show()
    }

    private fun deleteItem() {
        viewModel.deleteItem(item)
        findNavController().navigateUp()
    }

    private fun editItem() {
        val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
            getString(R.string.edit_fragment_title),    // "Edit Item"
            item.id
        )
        this.findNavController().navigate(action)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

}

 

이제 다시 앱을 실행하면 상세화면으로 이동했을 때 이름, 가격, 재고가 표시된다.

반응형
Comments