[Android] RecyclerView 사용시 흔하게 겪는 에러 해결방법 총정리!

2022. 3. 18. 13:36개발을 파헤치다/Android

반응형

 

Android RecyclerView 에러

Android 앱을 개발하면서 가장 필수적이고 많이 쓰이는 View가 있다면 저는 당연 RecyclerView를 얘기할 것 같습니다. 그만큼 많이, 흔하게 쓰이는 View인데요. 하지만 많이 쓰이는 것과 잘 사용하는 것은 전혀 다른 얘기입니다. Adapter, ViewHolder 이 정도만 개념을 익히고 구현해보면 금방 사용할 수 있긴 합니다. 그렇지만 실서비스를 개발하다 보면 여기에서 정말 크고 작은 문제가 많이 발생하죠. 이번에는 RecyclerView 사용할 때 겪을 수 있는 에러 상황들을 살펴보고 원인과 해결방법을 알려드리겠습니다. 

혹시 지금 에러를 맞닥뜨렸나요? 끝까지 한번 읽어보세요. 분명 도움이 될껍니다.

 

 

아이템 뒤섞임 문제 해결


RecyclerView를 사용하다 보면 스크롤 시 아이템이 마구 뒤섞이는 현상이 발생합니다. 예를 들어 리스트의 2번째 아이템의 데이터를 변경한 뒤 스크롤을 하면 아이템이 뒤죽박죽이 되는 것이죠. 이 문제를 어떻게 해결할지 살펴봅니다.

원인

RecyclerView는 리소스를 아끼기 위해 View를 재사용합니다. 이러한 이유 때문에 재사용하는 과정에서 기존 데이터가 덮어씌워지면서 뒤죽박죽이 되는 문제가 발생합니다.

해결

RecyclerView Adapter 코드에 아래 메서드를 추가합니다. 이렇게 하면 스크롤을 해도 아이템이 그대로 보이게 됩니다.

// 아이템 뒤섞임 문제 해결을 위한 메서드
    override fun getItemViewType(position: Int): Int {
        return position
    }

 

아이템 삭제 시 Index Out of Bounds 에러


RecyclerView에서 흔히 내부 데이터 관리를 위해 List를 사용합니다. 아이템을 삭제할 때 index값으로 기능을 구현하는데요.
이때 Index Out of Bounds 에러가 발생하는 경우가 있습니다. 데이터 삭제 후 RecyclerView에 이 사실을 알려주는 메서드를 사용해도 해당 에러가 발생할 수 있는데요. 원인과 해결 방법을 알아보도록 합니다.

원인

 

class ExampleAdapter : RecyclerView.Adapter<ExampleAdapter.ItemViewHolder>(){
    private var dataList = mutableListOf<Item>()
    ...
    
    // 삭제하는 메서드
    fun delete(position: Int){
        // Index Out of Bounds 에러 발생
        dataList.removeAt(position)
        // RecyclerView에 업데이트를 요청해도 발생
        notifyItemRemoved(position)
        notifyItemRangeChanged(position, dataList.size)
    }
}


이 문제가 발생하는 이유는 데이터 List에서 바뀐 데이터의 위치(index)와 RecyclerView 내부 onBindViewHolder 메서드에서 전달해주는 위치 값(position)의 차이 때문에 발생합니다. 이해하기 쉽도록 위의 그림을 가지고 얘기해봅시다. 먼저 처음에 데이터가 왼쪽 그림처럼 있다고 생각해봅니다. 그럼 onBindViewHolder의 position 값도 List에 맞게 설정이 됩니다. 하지만 제일 마지막 아이템인 3이 아닌 2를 지워버리게 되면 데이터 리스트의 위치 값이 (0,1,2)가 되어야 하는데 onBindViewHolder에서는 여전히 전의 값인 3을 리턴한다는 것입니다. 그렇기 때문에 이 상황에서 마지막 데이터를 삭제하려고 하면 Index Out of Bounds 에러가 발생하는 것이죠. 

이 에러는 삭제 후 RecyclerView에 변경사항을 알려줘도 발생합니다. 제가 테스트해보기로는 아래 메서드 모두 이 문제를 해결하지는 못했습니다.

// 아이템 전체를 업데이트
notifyDataSetChanged()

// 삭제한 아이템과 그 뒤의 아이템들을 업데이트
notifyItemRemoved(position)
notifyItemRangeChanged(position, list.size)

 

반응형

 

해결

해결방법은 List의 remove 메서드를 활용하는 것입니다. 위치 값으로 removeAt 메서드를 사용할 때에는 Index Out of Bounds 에러가 발생하지만 데이터 인스턴스를 직접 삭제하면 에러를 막을 수 있습니다.
위의 코드를 아래처럼 수정하면 됩니다.

class ExampleAdapter : RecyclerView.Adapter<ExampleAdapter.ItemViewHolder>(){
    private var dataList = mutableListOf<Item>()
    ...
    
    // 삭제하는 메서드
    fun delete(item: Item){
        // Index Out of Bounds 에러가 발생하지 않는다
        dataList.remove(item)
        
        // notifyItemRemoved나 notifyItemRangeChanged를 사용하려면
        // 위치값이 필요한데 List의 indexOf 메서드를 사용해도 -1이 리턴되어 에러가 발생한다
        // 따라서 데이터 전체 갱신을 요청한다. 
        notifyDataSetChanged()
    }
}



Adapter에서 ViewModel 사용 시 Live Data Event가 제대로 실행되지 않을 때

RecyclerView를 사용하다 보면 리스트에서 아이템 클릭 시 API 호출이나 로컬 데이터베이스에서 데이터를 가져오는 등 여러 가지 작업을 해야 할 때가 있습니다. MVVM 아키텍처에서는 Adapter내에서 ViewModel을 호출해야 하는데요. 이때 ViewModel을 잘못 사용하면 Live Data의 이벤트가 제대로 실행되지 않는 문제가 발생합니다. 즉, 콜백 함수가 호출이 아예 안 되는 것이죠. 이 문제를 어떻게 해결할지 한번 살펴보도록 합니다.

원인

class ExampleAdapter(val lifecycleOwner : LifeCycleOwner) : RecyclerView.Adapter<ExampleAdapter.ItemViewHolder>(){
    private lateinit var itemViewModel : SingleLiveEventViewModel
    ...
    
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
        val binding = ListItemBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        // ViewModel 초기화
        itemDeleteViewModel = SingleEventViewModel()
        return ItemViewHolder(binding)
    } 
    
    // 삭제하는 메서드
    fun delete(item: Item){
        itemViewModel.deleteItem(item)
        itemViewModel.successEvent.observe(lifecycleOwner, Observer{
            // 이렇게 구현한 경우 간헐적으로 콜백 함수가 실행이 되지 않음
            dataList.remove(item)
            notifyDataSetChanged()
        })
    }
}

위와 같이 구현하면 아이템들을 삭제하려고 할 때 됐다 안 됐다 하는 문제가 발생합니다. 원인은 바로 하나의 ViewModel로 여러 View Item을 처리하려고 해서 발생하는데요.

위 그림과 같은 구조로 이벤트가 발생하는데요. 하나의 ViewModel이 여러 Item의 이벤트를 처리하다 보니 ViewModel 과부하 문제가 발생합니다. RecyclerView의 수많은 아이템이 하나의 ViewModel에 이벤트 요청을 하기 때문에 과부하가 걸립니다. 이것 때문에 삭제 이벤트가 어떤 때에는 실행이 되고 어떤 때는 안됐던 것이죠.


해결

 


위 그림처럼 이벤트가 발생할 수 있도록 RecyclerView Item별로 ViewModel을 생성해줍니다. 이렇게 구현하면 삭제 이벤트 호출 시 각 Item별로 ViewModel 메서드가 실행되고 Live Data 이벤트를 받아 콜백 함수를 실행하기 때문에 과부하가 발생하지 않습니다. 

class ExampleAdapter(val lifecycleOwner : LifeCycleOwner) : RecyclerView.Adapter<ExampleAdapter.ItemViewHolder>(){
    ...
    
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder {
        val binding = ListItemBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        // ViewModel 초기화
        itemDeleteViewModel = SingleEventViewModel()
        return ItemViewHolder(binding)
    } 
    
    // 삭제하는 메서드
    fun delete(item: Item){
        val itemViewModel = SingleLiveEventViewModel()
        itemViewModel.deleteItem(item)
        itemViewModel.successEvent.observe(lifecycleOwner, Observer{
            // 이렇게 구현한 경우 간헐적으로 콜백 함수가 실행이 되지 않음
            dataList.remove(item)
            notifyDataSetChanged()
        })
    }
}





반응형