마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

리사이클러뷰 아이템 추가화면이 업데이트 되지않습니다.

0 추천

기존에는 한화면에서 부모아이템과 상세아이템을 추가했었는데 진행하다보니

뷰에 조금 변화가 있어서 다른 화면에서 아예 리스트를 만들어서 넘겨받아서 업데이트 하는 방식으로

변경하여 다른화면에서 아이템을 추가하는 방식을 만들고 있는데요,

 

사실 이전에는 멀티타입뷰를 사용해서 했다면 이번에는 그냥 상세아이템만 추가하면돼서

기존의 리사이클러뷰랑 완전히 똑같죠. 간단해졌다는 말이죠..

그런데 문제가 이상하게 아이템 추가를 하면 화면이 업데이트 되지 않는다는 겁니다..

 

아이템을 클릭하면 아이템이 단하나만 추가되고 이후로는 추가되지 않습니다.

아이템 추가버튼을 클릭했을때를 디버깅을 해보았는데 뷰모델에서 MutableList까지 추가되는것은

확인을했고

추가되면 아이템의 변경사항이 생긴것이니 obeserve까지는 호출이 되는데 submitList는 호출이 되지

않는것 같습니다..새 리스트와 이전리스트를 비교하기위한 DiffUtil 또한 호출되지 않는것같구요..

그런데 바텀 메뉴로 다른화면을 갔다가 다시 돌아오면 화면 아이템이 업데이트 되어있네요..

뭐가 문제인가요..일반 리사이클러뷰인데 갑자기 안되고 이유를 못찾으니 답답하네요..

sealed class

sealed interface RoutineItem {
    val id: String

    data class Header(
        override val id: String = UUID.randomUUID().toString(),
        val workout: String,
        val unit: String,
    ) : RoutineItem

    data class Detail(
        override val id: String = UUID.randomUUID().toString(),
        val set: Int,
        var weight: String = "",
        var reps: String = "",
    ) : RoutineItem
}

fragment

class WriteRoutineFragment2 : Fragment() {
    private var _binding : FragmentWriteRoutine2Binding? = null
    private val binding get() = _binding!!
    val args: WriteRoutineFragment2Args by navArgs()
    lateinit var workout: String
    private lateinit var adapter: DetailAdapter
    private val vm : DetailViewModel by viewModels { DetailViewModelFactory() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        args?.let {
            workout = it.workout.toString()
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentWriteRoutine2Binding.inflate(inflater, container, false)

        adapter = DetailAdapter()

        binding.apply {
            rv.adapter = adapter
            rv.itemAnimator = null
            header.add.setOnClickListener {
                vm.addDetail()
            }
            header.delete.setOnClickListener {
                vm.deleteDetail()
            }
            header.toggleButton.addOnButtonCheckedListener { _, checkedId, _ ->
                when(checkedId) {
                    R.id.kg -> Toast.makeText(context, "checkedId: $checkedId", Toast.LENGTH_SHORT).show()
                    R.id.lb -> Toast.makeText(context, "checkedId: $checkedId", Toast.LENGTH_SHORT).show()
                }
            }
        }
        return binding.root
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        vm.items.observe(viewLifecycleOwner) { newList ->
            adapter.submitList(newList) // 새로운 리스트를 넘김.
        }
    }
}

 

adapter

class DetailAdapter
    : ListAdapter<RoutineItem.Detail, DetailAdapter.ViewHolder>(DetailDiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            ItemRoutineDetailBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    }

    inner class ViewHolder(val binding: ItemRoutineDetailBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item: RoutineItem.Detail) {

        }
    }
}

 

DiffUtil

class DetailDiffCallback : DiffUtil.ItemCallback<RoutineItem.Detail>() {
    override fun areItemsTheSame(
        oldItem: RoutineItem.Detail,
        newItem: RoutineItem.Detail
    ): Boolean = (oldItem.id == newItem.id)

    override fun areContentsTheSame(
        oldItem: RoutineItem.Detail,
        newItem: RoutineItem.Detail
    ): Boolean = (oldItem == newItem)
}

 

codeslave (3,940 포인트) 님이 2022년 1월 3일 질문
codeslave님이 2022년 1월 3일 수정

1개의 답변

+1 추천
 
채택된 답변

아마도 문제는 ViewModel에 있을 겁니다. ViewModel에서 ArrayList같은 MutableList 타입을 사용하게 되면 DiffUtil에서 그 ArrayList대한 포인터를 가지고 있기 때문에 최초에 submitList()할 때만 아이템 변경이 반영이 될 겁니다. ViewModel에서 LiveData를 통해 값을 전달할 때는 readonly인 List타입의 복사본을 넘기도록 변경해 보세요. 이게 Immutable type이 Mutable type 보다 선호되는 이유이기도 합니다.

리스트를 조작하는 클래스에서 아래처럼 리턴타입이 Immutable이 되도록 만드세요.

private val items = arrayListOf<WorkoutModel>()

fun addDetail(item: WorkOutModel): List<WorkoutModel> {
     items.add(item)
     return Collections.unmodifiableList(items)
}

 

spark (227,530 포인트) 님이 2022년 1월 3일 답변
codeslave님이 2022년 1월 4일 채택됨
아.. 같은 주소.. DiffUtil에 디버깅이 안되니 전혀 그 생각을 못했네요..
선생님 그런데 Fragment 코드를 보시면 아시겠지만
observe.items 에서 items가 MutableLiveData입니다. items에 변화가 생기면(아이템이 추가및 삭제가되면) 감지해서 submitList를 동작시키죠.

그런데 이때, observe { newList -> ... } 의 인자로 변경된 items가 될텐데 이것을 그대로 전달 할 수는 없을까요? 선생님의 경우에는 관찰되고 있는 LiveData가 아니라
아예 리스트를 하나 만들고 그것을 변환시켜서 리턴을 해주는 방식을 사용해주시고 있으시잖아요?
그냥 observe { } 에 오는 인자 그대로 넘겨주는 방법으로 해결할 수 있는 법은 없나요?

새로운 리스트를 만들어서 따로 전달하지 않는이상 LiveData를 전달하게 되면 무조건 같은 주소를 가리키게 되므로 방도가 없을까요?
제가 이걸 왜 물어보냐하면.. 이렇게되면 items.observe { newList -> } 에서
newList 쓸모없는 인자가 되어버리는것같고..업데이트된 인자가 있음에도 굳이 메소드를 호출한다는 점이 찝찝? 한느낌이 들어서요.

그리고 ViewModel내에서도 LiveData가 변화됐음을 알리기위해서 의도적으로 postValue를 진행시켜줘야하는것도 그렇구요..
class DetailViewModel : ViewModel() {
    private val _items: MutableLiveData<List<RoutineItem2.Detail>> = MutableLiveData()
    private val list: ArrayList<RoutineItem2.Detail> = arrayListOf()
    val items = _items

    fun addDetail() {
        val item = RoutineItem2.Detail(set = list.size+1)
        list.add(item)

        _items.postValue(list)
    }

    fun deleteDetail() {
        list.removeLast()
        _items.postValue(list)
    }

    fun getUpdatedList() : List<RoutineItem2.Detail>{
        return Collections.unmodifiableList(list)
    }
}

ViewModel 코드입니다. (선생님이 사용하신 코드 추가)

그런데 선생님이 사용하신 Collections.unmodi~~ 를 사용하니 결과가 그대로네요..아예 getUpdatedList 안에서 새로운 리스트를 만들어서 리턴하면 화면이 정상적으로 업데이트 되구요..
먼저 DiffUtil은 디버그가 가능하구요. DiffUtil이 내부적으로 리스트 변수를 가지고 이전 아이템을 캐싱하는 건 디버깅을 통해서 찾을 겁니다. DiffUtil이 가진 변수랑 submitList로 들어오는 변수랑 같을 경우,  submitList는 리사이클러뷰를 업데이트 하지 않게 됩니다. 따라서 현재 님의 코드로느 아무리 해도 변경사항이 반영이 되지 않을 겁니다.
저같은 경우는 Domain레이어가 분리되어 있고, Domain과 View에 사용하는 data class가 각각 따로 존재하고 맵핑을 통해 사용하기 때문에 항상 복사된 List를 사용하도록 하고 있습니다.
심플하게 위에서 말씀드린 Collections.unmodifiableList(items) 을 추가하시거나 map을 사용하세요.

 fun addDetail() {
        val item = RoutineItem2.Detail(set = list.size+1)
        list.add(item)
        postList()
    }

    fun deleteDetail() {
        list.removeLast()
        postList()
    }

private fun postList() {
     val cachedItems = Collections.unmodifiableList(list.value  ?: emptyList())
     _items.postValue(cachedItems)
}

또는

private fun postList() {
     val cachedItems = list.value?.map{it}
     _items.postValue(cachedItems)
}

위와 방법이 아니면, _items.value를  복사해서 사용할 수도 있습니다. 이 경우에는 굳이 list 프로퍼티가 필요없을 것 같아요.
 
private val list: List<RoutineItem2.Detail>
     get() = _items.value ?: emptyList()

fun addDetail() {
    val item = RoutineItem2.Detail(set = list.size+1)
    _items.postValue(list.plus(item))
}

fun deleteDetail() {
   _items.postValue(list.dropLast())
}
감사합니다.  Collections.unmodifiableList()은 여전히 안되어서 map방식을 썼습니다..그리고 선생님 코드에서 list가 arrayList라 value는 안되어서 조금 수정해서 썻습니다..
그럼, 해결이 되신건가요? 해결이 되신거면, 같은 문제가 있는 분들이 해결방법을 알 수 있도록 답변 채택을 해주시면 감사하겠습니다.
...