안드로이드

DataBinding, BindingAdapter를 사용할 때 무한루프

pjm1n 2025. 8. 2. 20:12

🚨 문제 상황

DataBinding을 사용하며 BindingAdapter를 통해 xml의 속성을 커스텀 하던 중 viewHolder에 데이터를 무한으로 바인딩하는 문제가 발생했다.

👿  원인

  1. submitList() 호출
    	 private fun setupObservers() {
            viewModel.faqUiState.observe(viewLifecycleOwner) { state ->
                when (state) {
                    FAQUiState.InitialLoading -> {}
                    FAQUiState.Refreshing -> {}
                    is FAQUiState.Success -> {
                        adapter.submitList(state.faqs)
                    }
    
                    is FAQUiState.Error -> {
                        showErrorSnackBar(state.throwable)
                    }
                }
            }
        }
    
    확장버튼을 클릭해 isExpanded가 true로 바뀌면 submitList()를 통해 ViewHolder의 뷰를 다시 바인딩한다.
  2. onBindingViewHolder() 호출
     fun bind(item: FAQItemUiModel) {
            binding.faqItem = item
            }
    
    ViewHolder가 재사용되며, 다음과 같이 DataBinding 변수에 새데이터가 바인딩된다.
  3. DataBinding → @BindingAdapter 실행
    @BindingAdapter("answerVisibility")
    fun setAnswerVisibility(
        textView: TextView,
        isExpanded: Boolean?,
    ) {
        isExpanded ?: return
        if (isExpanded) textView.visibility = View.VISIBLE else textView.visibility = View.GONE
    }
    
    @BindingAdapter("expandButton")
    fun setExpandButton(
        imageView: ImageView,
        isExpanded: Boolean?,
    ) {
        isExpanded ?: return
        if (isExpanded) {
            imageView.setImageResource(R.drawable.ic_chevron_up)
        } else {
            imageView.setImageResource(R.drawable.ic_chevron_down)
        }
    }
    
    XML에 지정된 app:expandButton="@{item.isExpanded}" 와 app:answerVisibility="@{item.isExpanded}”로 인해 해당 값인 isExpanded가 바뀌거나 바인딩 되면 다음 함수들이 실행된다.
  4. 뷰 상태 갱신 → 내부 invalidate()→ layout pass 
    • setImageResource()는 내부적으로 invalidate() → requestLayout()을 호출한다.
    • setVisibility()도 마찬가지로 requestLayout()을 유발한다.
    • 이 requestLayout()은 부모인 RecyclerView에도 영향을 준다. 즉, 뷰가 바뀌었다고 RecyclerView가 판단해서 다시 ViewHolder 바인딩을 유도한다.
      • requestLayout(): 현재 뷰와 그 자식 뷰들에 대해 레이아웃(크기, 위치)을 재계산 요청하는 함수
      • invalidate() : 뷰를 다시 그려야 함을 요청하는 함수
  5. RecyclerView가 다시 ViewHolder 바인딩
    • 이 시점에서 RecyclerView는 이 ViewHolder가 바뀌었다고 착각해서 다시 onBindViewHolder()를 호출한다.
    • DataBinding을 다시연결
    • @BindingAdapter 다시 호출

🔑 결론

  • 데이터가 안 바뀌었는데도 setImageResource()와 setVisibility()를 무조건 호출
  • 그 호출이 뷰의 레이아웃 상태를 바꾼다고 판단되면 RecyclerView는 해당 뷰가 변경됨으로 간주한다.
  • 그래서 submitList() 같은 호출이 없어도, 내부적으로 다시 ViewHolder를 재바인딩한다.
사용자 클릭 → 데이터 변경 → submitList(state.faqs)
          ↓
  onBindViewHolder() → fAQItem 바인딩
          ↓
   DataBinding 감지 → @BindingAdapter 호출
          ↓
   setImageResource / setVisibility 호출
          ↓
   내부적으로 requestLayout() 발생
          ↓
   RecyclerView가 이 뷰를 다시 바인딩하도록 판단
          ↓
   onBindViewHolder() → 다시 위로 루프

 

덕분에 3시간 넘게 삽질하다가 이제야 해결했다. 갈 길이 멀다. 끗.