일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 스택 큐 차이
- 서비스 쓰레드 차이
- rxjava cold observable
- 2022 플러터 설치
- android ar 개발
- 2022 플러터 안드로이드 스튜디오
- 안드로이드 라이선스
- rxjava disposable
- 객체
- 안드로이드 레트로핏 사용법
- ar vr 차이
- 안드로이드 유닛 테스트 예시
- 안드로이드 유닛 테스트
- 서비스 vs 쓰레드
- 자바 다형성
- Rxjava Observable
- 안드로이드 os 구조
- rxjava hot observable
- 스택 자바 코드
- ANR이란
- 안드로이드 라이선스 종류
- android retrofit login
- 안드로이드 레트로핏 crud
- jvm 작동 원리
- 클래스
- jvm이란
- 플러터 설치 2022
- 안드로이드 유닛테스트란
- 큐 자바 코드
- 멤버변수
- Today
- Total
나만을 위한 블로그
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 4 - 본문
이제 마지막으로 아이템 수정, 삭제 기능을 구현하고 마무리한다. 이전 포스팅들은 아래와 같다.
https://onlyfor-me-blog.tistory.com/556
https://onlyfor-me-blog.tistory.com/558
https://onlyfor-me-blog.tistory.com/559
먼저 뷰모델에 updateItem(), sellItem()을 추가한다.
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
}
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
}
copy()라는 함수가 눈에 띄는데 이 함수는 data class의 모든 인스턴스를 통해 사용할 수 있는 함수다. 전체 속성 중 일부만 변경하고 나머지는 그대로 두면서 객체를 복사하는 함수다. 1개를 없애면 재고를 -1 시켜야 하는데 그 처리를 위해 사용한다.
그리고 ItemDetailFragment의 bind() 안에서 sellItem에 대한 클릭 리스너를 만들고 뷰모델의 sellItem()을 호출한다.
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()
sellItem.setOnClickListener {
viewModel.sellItem(item)
}
}
}
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
}
}
그리고 재고가 0이면 판매할 게 없다는 뜻이니 sell 버튼을 누르지 못하게 만드는 게 좋다. InventoryViewModel에서 재고가 0보다 큰지 확인하는 isStockAvailable()을 추가한다.
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
}
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
}
그리고 ItemDetailFragment로 다시 이동해서 bind() 안의 sellItem 클릭 리스너 바로 위에 방금 만든 함수를 사용하는 코드를 한 줄 추가한다.
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()
sellItem.isEnabled = viewModel.isStockAvailable(item)
sellItem.setOnClickListener {
viewModel.sellItem(item)
}
}
}
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
}
}
이제 재고가 0이면 sell 버튼의 isEnabled 값이 false가 되고 1 이상이 되면 true가 되어 사용할 수 있게 된다.
삭제 기능도 구현한다. InventoryViewModel에 deleteItem()을 추가한다.
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
}
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(item)
}
}
}
그리고 ItemDetailFragment의 빨간 줄이 나오던 showConfirmationDialog()를 확인한다. 함수를 만들었으니 더 이상 빨간 줄이 나오지 않을 것이다.
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()
}
그리고 위로 스크롤해서 bind() 안에 deleteItem에 클릭 리스너를 추가하고 showConfirmationDialog()를 호출하게 한다.
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()
sellItem.isEnabled = viewModel.isStockAvailable(item)
sellItem.setOnClickListener {
viewModel.sellItem(item)
}
deleteItem.setOnClickListener {
showConfirmationDialog()
}
}
}
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
}
}
앱을 실행해서 확인해 보면 삭제가 잘 되는 걸 확인할 수 있다. 이제 마지막 기능인 아이템 수정 기능을 구현한다.
ItemDetailFragment()의 editItem()을 보면 된다.
private fun editItem() {
val action = ItemDetailFragmentDirections.actionItemDetailFragmentToAddItemFragment(
getString(R.string.edit_fragment_title), // "Edit Item"
item.id
)
this.findNavController().navigate(action)
}
이걸 누르면 액션바에 넣을 이름과 아이템의 id값을 AddItemFragment로 옮기게 된다.
bind()로 다시 이동해서 editItem에 클릭 리스너를 추가하고 클릭 시 editItem()을 호출하게 한다.
private fun bind(item: Item) {
binding.apply {
itemName.text = item.itemName
itemPrice.text = item.getFormattedPrice()
itemCount.text = item.quantityInStock.toString()
sellItem.isEnabled = viewModel.isStockAvailable(item)
sellItem.setOnClickListener {
viewModel.sellItem(item)
}
deleteItem.setOnClickListener {
showConfirmationDialog()
}
editItem.setOnClickListener {
editItem()
}
}
}
이제 AddItemFragment로 이동해서 수정 버튼을 눌러 이동했을 경우의 처리를 추가한다.
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
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.FragmentAddItemBinding
import com.example.kotlinprac.room.data.Item
class AddItemFragment : Fragment() {
private val navigationArgs: ItemDetailFragmentArgs by navArgs()
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory((activity?.application as InventoryApplication).database.itemDao())
}
lateinit var item: Item
private var _binding: FragmentAddItemBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddItemBinding.inflate(inflater, container, false)
return binding.root
}
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)
}
}
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
private fun addNewItem() {
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
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()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// 키보드 숨기기
val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as
InputMethodManager
inputMethodManager.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0)
_binding = null
}
}
bind() 안을 보면 price 프로퍼티에 가격을 소수점 이하 2자리로 반올림하게 하는 처리가 들어가 있다.
그리고 이 변환된 price를 포함한 Item 안의 정보들을 가져와 텍스트뷰에 set한다.
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)
saveAction.setOnClickListener {
updateItem()
}
}
}
그리고 onViewCreated()의 로직이 조금 바뀌어 있다.
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()
}
}
}
네비게이션 인자로 받은 itemId 값을 통해 0보다 클 경우 이전 화면에서 선택한 아이템의 정보들을 텍스트뷰에 바인딩하고, 그게 아니면 새 아이템을 추가하도록 한다.
이제 아이템 정보를 수정하고 save를 누르면 Room DB 안의 정보를 업데이트하는 처리를 구현한다. InventoryViewModel에 getUpdatedItemEntry(), updateItem()을 만든다.
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
}
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(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()
)
}
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
updateItem(updatedItem)
}
}
이제 AddItemFragment로 돌아가서 코드를 몇 줄 추가한다.
package com.example.kotlinprac.room
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
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.FragmentAddItemBinding
import com.example.kotlinprac.room.data.Item
class AddItemFragment : Fragment() {
private val navigationArgs: ItemDetailFragmentArgs by navArgs()
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory((activity?.application as InventoryApplication).database.itemDao())
}
lateinit var item: Item
private var _binding: FragmentAddItemBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddItemBinding.inflate(inflater, container, false)
return binding.root
}
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)
saveAction.setOnClickListener {
updateItem()
}
}
}
private fun isEntryValid(): Boolean {
return viewModel.isEntryValid(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
}
private fun addNewItem() {
if (isEntryValid()) {
viewModel.addNewItem(
binding.itemName.text.toString(),
binding.itemPrice.text.toString(),
binding.itemCount.text.toString()
)
val action = AddItemFragmentDirections.actionAddItemFragmentToItemListFragment()
findNavController().navigate(action)
}
}
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)
}
}
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()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// 키보드 숨기기
val inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as
InputMethodManager
inputMethodManager.hideSoftInputFromWindow(requireActivity().currentFocus?.windowToken, 0)
_binding = null
}
}
이제 앱을 실행하면 아이템을 클릭 시 상세 화면으로 이동하고, sell 버튼을 누르면 수정 화면으로 이동해서 수정할 수 있게 된다. save를 누르면 리사이클러뷰 화면으로 이동해서 아이템이 변경된 걸 확인할 수 있다.
아래는 코드랩에서 제공하는 전체 완성본 코드들이다. AddItemFragment는 바로 위에 있으니 생략한다.
프로젝트 경로가 다른 부분은 참고해서 수정하면 된다.
import androidx.lifecycle.*
import com.example.kotlinprac.room.data.Item
import com.example.kotlinprac.room.data.ItemDao
import kotlinx.coroutines.launch
import kotlin.coroutines.coroutineContext
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
}
fun retrieveItem(id: Int): LiveData<Item> {
return itemDao.getItem(id).asLiveData()
}
private fun updateItem(item: Item) {
viewModelScope.launch {
itemDao.update(item)
}
}
fun sellItem(item: Item) {
if (item.quantityInStock > 0) {
val newItem = item.copy(quantityInStock = item.quantityInStock - 1)
updateItem(newItem)
}
}
fun isStockAvailable(item: Item): Boolean {
return (item.quantityInStock > 0)
}
fun deleteItem(item: Item) {
viewModelScope.launch {
itemDao.delete(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()
)
}
fun updateItem(
itemId: Int,
itemName: String,
itemPrice: String,
itemCount: String
) {
val updatedItem = getUpdatedItemEntry(itemId, itemName, itemPrice, itemCount)
updateItem(updatedItem)
}
}
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
}
}
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()
sellItem.isEnabled = viewModel.isStockAvailable(item)
sellItem.setOnClickListener {
viewModel.sellItem(item)
}
deleteItem.setOnClickListener {
showConfirmationDialog()
}
editItem.setOnClickListener {
editItem()
}
}
}
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
}
}
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
}
}
}
}
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)
}
}
}
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController
import com.example.kotlinprac.R
class RoomMainActivity : AppCompatActivity(R.layout.activity_room_main) {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean =
navController.navigateUp() || super.onSupportNavigateUp()
}
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)
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface ItemDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: Item)
@Update
suspend fun update(item: Item)
@Delete
suspend fun delete(item: Item)
@Query("SELECT * FROM item WHERE id = :id")
fun getItem(id: Int): Flow<Item>
@Query("SELECT * FROM item ORDER BY name ASC")
fun getItems(): Flow<List<Item>>
}
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Item::class], version = 1, exportSchema = false)
abstract class ItemRoomDatabase: RoomDatabase() {
abstract fun itemDao(): ItemDao
companion object {
@Volatile
private var INSTANCE: ItemRoomDatabase? = null
fun getDatabase(context: Context): ItemRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
ItemRoomDatabase::class.java,
"item_database"
)
.fallbackToDestructiveMigration()
.build()
return instance
}
}
}
}
import android.app.Application
import com.example.kotlinprac.room.data.ItemRoomDatabase
class InventoryApplication: Application(), ImageLoaderFactory {
val database: ItemRoomDatabase by lazy {
ItemRoomDatabase.getDatabase(this)
}
}
'Android' 카테고리의 다른 글
[Android] Flow, LiveData를 써서 네트워크 연결 상태를 확인하는 방법 (0) | 2023.01.20 |
---|---|
[Android] 앱 시작 시간의 종류 (Cold / Warm / Hot Start) (0) | 2023.01.19 |
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 3 - (0) | 2022.12.13 |
[Android] Jetpack Navigation, Room DB, Flow 같이 사용하기 - 2 - (0) | 2022.12.13 |
[Android] Flow vs LiveData (0) | 2022.12.12 |