본문 바로가기

안드로이드

[안드로이드] 예제: MVVM+AAC를 이용한 RecyclerView 10 - Swipe, Drag&Drop으로 아이템 삭제 및 순서 변경 버그 픽스

개요

이전 문서의 개발까지에서는 두 동작을 각각 수행하면 전혀 문제가 없었다. 예를들어 Drag&Drop하고 리스트를 업데이트하거나 swipe만 하여 아이템을 삭제하거나. 하지만 리스트를 업데이트 한 뒤 스와이프하여 삭제하면 애니메이션이 의도대로 움직이지 않는다. 따라서 이번 문서에서는 해당 문제를 다룬다.


문제점

기존의 이동과 삭제하는 경우의 코드를 살펴보자.

override fun onItemMove(fromPosition:Int, toPosition:Int) {
        if(!set) {	        
            list.addAll(currentList)
            set=true
        }
  
        val temp = list.get(fromPosition).itemOrder	        
        list.get(fromPosition).itemOrder=list.get(toPosition).itemOrder	     
        list.get(toPosition).itemOrder = temp	     

        Collections.sort(list)	       
        notifyItemMoved(fromPosition,toPosition)	      
    }
    
override fun onItemDismiss(position: Int) {
        viewModel.viewModelScope.launch ( Dispatchers.IO ){	        
            viewModel.delete(getItem(position))
            set=true
        }
    }	

아이템을 움직이기만 하고 프래그먼트를 나가는 경우, 아이템을 삭제하기만 하고 프래그먼트를 나가는 경우 모두 문제가 되지 않는다. 문제가 되는 경우는 아이템을 움직이고 삭제를 할 경우 제대로 반영이 되지 않는다. 우선 아이템을 움직일 경우 리스트 어답터에 실제 세팅되어 있는 list와 화면상 보이는 list와는 차이가 있다. 그렇기에 삭제를 수행할 경우 엉뚱한 아이템을 DB에서 삭제하게 된다. 

 

그래서 위 문제를 해결 하기 위해 아이템 움직이는 동작이 끝난뒤 DB를 업데이트하고 아이템을 삭제하는 코드도 아래처럼 수정을 했다.

  override fun onItemMove(fromPosition:Int, toPosition:Int) {
        if(!set) {
            list.addAll(currentList)
            set=true
        }
        //이거는 order만 바꾸어줌
        val temp = list[fromPosition].itemOrder
        list[fromPosition].itemOrder=list[toPosition].itemOrder
        list[toPosition].itemOrder = temp

        Collections.sort(list)
        notifyItemMoved(fromPosition,toPosition)

    }

    override fun onItemMoveFinished() {
        viewModel.viewModelScope.launch(Dispatchers.IO) {
            viewModel.updateAll(list)
        }
    }
    
     override fun onItemDismiss(position: Int) {
        if (!set) {
            list.addAll(currentList)
            set=true
            set = true
        }

        val index=currentList.indexOf(list[position])
        list.removeAt(position)

        viewModel.viewModelScope.launch(Dispatchers.IO) {
            viewModel.delete(getItem(index))
        }
    }

 위 코드는 논리상 문제가 없다. 하지만 애니메이션이 약간 이상하게 동작한다. 결과로 나오는 UI는 DB에 존재하는 리스트와 완벽하게 일치하지만 아이템이 부적절하게 움직인다. 이유는 notifyItemMoved와 DiffUtil에 의해 발생하는 것으로 추측된다. notifyItemMoved로 UI에 그려진 리스트를 변경하고 실제 아이템을 드랍할 경우 DB에 리스트가 업데이트 되는데 이 과정에서 DiffUtil 또한 수행되어 다시 한번 아이템을 그린다. 이런 불필요한 애니메이션을 없애려고 노력했으나 결국 나는 ListAdapter 대신 RecyclerView.Adapter를 사용하기로 결정했다.

 

 

 

이 어플 애니메이션이 약간 이상한데..?

 


ListAdapter에서 RecyclerView.Adpater로 변경

문제는 매우 간단해졌다. 어답터에서는 삭제된 아이템을 저장하는 리스트와 업데이트된 아이템들이 저장된 리스트를 갖고 있다가, SecondFragment에서 onPause()가 호출될 때 해당 리스트들을 호출하여 DB에 적절히 반영한다.

 

TodoAdapter.kt 수정

class TodoAdapter(application: Application): RecyclerView.Adapter<TodoAdapter.ViewHolder>(), ItemTouchHelperListener {
    private var list = ArrayList<Todo>()
    private var deletedItemList = ArrayList<Todo>()
    var set = false


    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val binding =
            DataBindingUtil.inflate<ItemTodoBinding>(layoutInflater, viewType, parent, false)
        return ViewHolder(binding)
    }

    override fun getItemViewType(position: Int): Int {
        return R.layout.item_todo
    }

    fun getList() : ArrayList<Todo> {
        return list
    }
    fun getDeletedItemList() : ArrayList<Todo> {
        return deletedItemList
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val todo = list[position]
        holder.bind(todo)
    }


    class ViewHolder(private val binding: ItemTodoBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(todo: Todo) {
            binding.todo = todo
            binding.executePendingBindings() //데이터가 수정되면 즉각 바인딩
        }

    }

    override fun onItemMove(fromPosition:Int, toPosition:Int) {
        val temp = list[fromPosition].itemOrder
        list[fromPosition].itemOrder=list[toPosition].itemOrder
        list[toPosition].itemOrder = temp

        Collections.sort(list)
        notifyItemMoved(fromPosition,toPosition)
    }


    fun setList(list:ArrayList<Todo>){
        this.list=list
        notifyDataSetChanged()
    }


    override fun onItemDismiss(position: Int) {
        deletedItemList.add(list[position])
        list.removeAt(position)
        notifyItemRemoved(position)
    }

    override fun getItemCount(): Int {
        return list.size
    }
}


 

SecondFragment.kt

class SecondFragment : Fragment() {
    ...
    override fun onPause() {
        super.onPause()
        viewModel.viewModelScope.launch(Dispatchers.IO){
            if(adapter.getList().size>0) {
                viewModel.updateAll(adapter.getList())
            }
            if(adapter.getDeletedItemList().size > 0){
                val deletedItemList = adapter.getDeletedItemList()
                for(i in (deletedItemList.size-1) downTo 0)
                    viewModel.delete(deletedItemList[i])
            }
        }
    }
}

리뷰

어떻게 문제점은 해결을 했지만 굉장히 아쉬움이 많이 남는다. 어쨋든 ListAdapter를 기반으로 하여 구현하고 싶었지만 실패했다. 새로운 기술을 적용하는 것은 레퍼런스가 별로 없다보니 꽤나 어려운 일이라고 느꼈다. 커밋 7962dba가 ListAdapter를 사용하며 애니메이션이 약간 이상하고 DB 상태는 정확히 반영하는 커밋이니 나중에 기회가 된다면 다시 시보해보자.