안드로이드
DataBinding, BindingAdapter를 사용할 때 무한루프
pjm1n
2025. 8. 2. 20:12
🚨 문제 상황
DataBinding을 사용하며 BindingAdapter를 통해 xml의 속성을 커스텀 하던 중 viewHolder에 데이터를 무한으로 바인딩하는 문제가 발생했다.
👿 원인
- submitList() 호출
확장버튼을 클릭해 isExpanded가 true로 바뀌면 submitList()를 통해 ViewHolder의 뷰를 다시 바인딩한다.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) } } } } - onBindingViewHolder() 호출
ViewHolder가 재사용되며, 다음과 같이 DataBinding 변수에 새데이터가 바인딩된다.fun bind(item: FAQItemUiModel) { binding.faqItem = item } - DataBinding → @BindingAdapter 실행
XML에 지정된 app:expandButton="@{item.isExpanded}" 와 app:answerVisibility="@{item.isExpanded}”로 인해 해당 값인 isExpanded가 바뀌거나 바인딩 되면 다음 함수들이 실행된다.@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) } } - 뷰 상태 갱신 → 내부 invalidate()→ layout pass
- setImageResource()는 내부적으로 invalidate() → requestLayout()을 호출한다.
- setVisibility()도 마찬가지로 requestLayout()을 유발한다.
- 이 requestLayout()은 부모인 RecyclerView에도 영향을 준다. 즉, 뷰가 바뀌었다고 RecyclerView가 판단해서 다시 ViewHolder 바인딩을 유도한다.
- requestLayout(): 현재 뷰와 그 자식 뷰들에 대해 레이아웃(크기, 위치)을 재계산 요청하는 함수
- invalidate() : 뷰를 다시 그려야 함을 요청하는 함수
- 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시간 넘게 삽질하다가 이제야 해결했다. 갈 길이 멀다. 끗.