다음은 프로젝트 진행에 있어 문제 해결에 많은 어려움을 겪었던 코드이다.

class PicturesAdapter() : RecyclerView.Adapter<PictureViewHolder>() {
    private lateinit var itemBinding: ItemPhotoCheckingBinding
    private val pictures : ArrayList<Uri> = ArrayList()


    inner class PictureViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView){
        fun bind(pos : Int){
            val targetUri = pictures[pos]
            GlideApp.with(itemView.context)
                .load(targetUri)
                    .into(itemBinding.photo)
            itemBinding.clearBtn.setOnClickListener {
                removeItem(targetUri)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PictureViewHolder {
        itemBinding = ItemPhotoCheckingBinding.inflate(LayoutInflater.from(parent.context))
        return PictureViewHolder(itemBinding.root)
    }

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

    override fun onBindViewHolder(holder: PictureViewHolder, position: Int) {
        holder.bind(position)
    }


    private fun removeItem(targetUri :Uri){
        val pos = pictures.indexOf(targetUri)
        pictures.removeAt(pos)
        notifyItemRemoved(pos)
    }

    fun addPicture(uri : Uri){
        pictures.add(0,uri)
        notifyItemInserted(0)
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

처음에는 bind(pos : Int) 시에 position이 fix 되어 문제가 발생한다고 생각하였다.

absoluteAdapterPosition, bindingAdapterPosition 등을 사용하여 해결하려 했으나 문제는 viewBinding에 있었다.

 

해당 코드를 살펴보면 onCreateViewHolder에서 바인딩 인스턴스를 생성하여 root 레이아웃을 itemView로 건네고 있습니다.여기까진 문제가 없을 수 있으나 itemView 내부의 view ( photo, clearBtn ) 에 대해서 onCreateViewHolder에서 생성한 동일한 바인딩 객체에 참조 하는 것을 확인할 수 있습니다. RecyclerView의 목적처럼 뷰홀더를 재사용하는것이 불가능해집니다.

 

해결책

1. itemView를 넘기는 기존의 방식을 사용하여 해결 (findViewByID) -  viewbinding을 사용하는 의미가 퇴색됨

    inner class PictureViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            fun bind(pos : Int){
                val _photo = itemView.findViewById<ImageView>(R.id.photo)
                val _clearBtn = itemView.findViewById<ImageView>(R.id.clearBtn)

                val targetUri = pictures[pos]
                GlideApp.with(itemView.context)
                    .load(targetUri)
                    .into(_photo)
                _clearBtn.setOnClickListener {
                    imageManager.deleteImage(targetUri)
                    removeItem(targetUri)
                }
            }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PictureViewHolder {
        itemBinding = ItemPhotoCheckingBinding.inflate(LayoutInflater.from(parent.context))
        return PictureViewHolder(itemBinding.root)
    }

 

 

2. viewBinding을 활용한 ViewHolder 사용 ( 권장 )

    inner class PictureViewHolder(private val itemBinding: ItemPhotoCheckingBinding) : RecyclerView.ViewHolder(itemBinding.root){
        fun bind(pos : Int){
            val targetUri = pictures[pos]
            GlideApp.with(itemView.context)
                .load(targetUri)
                    .into(itemBinding.photo)
            itemBinding.clearBtn.setOnClickListener {
                imageManager.deleteImage(targetUri)
                removeItem(targetUri)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PictureViewHolder {
        itemBinding = ItemPhotoCheckingBinding.inflate(LayoutInflater.from(parent.context))
        return PictureViewHolder(itemBinding)
    }

 

설정

 

사용하고자 하는 module의 build.gradle에 추가

android {
        ...
        viewBinding {
            enabled = true
        }
    }

 

사용

 

모듈에 뷰 결합을 사용하도록 설정되면 모듈에 포함된 각 XML 레이아웃 파일의 결합 클래스가 생성됩니다.

(루트 뷰에  tools:viewBindingIgnore="true" 속성이 포함된 경우 제외)

결합 클래스의 이름은 XML 파일의 이름을 카멜 표기법으로 변환하고 끝에 'Binding'을 추가하여 생성되며, 루트뷰와 ID가 있는 모든 뷰의 참조가 포함됩니다.

 

 

                 activity_main.xml           →     activityMainBinding

                 fragment_example.xml  →     FragmentExampleBinding

 

 

 

 

 

 

Activity

      

기존 

class MainActivity : AppCompatActivity() {
	private lateinit var viewExample: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        viewExample = findViewById(R.id.view_example)
    }
}

 

뷰 바인딩 방식

class MainActivity : AppCompatActivity() {
	private lateinit var viewExample: TextView

	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater) // 뷰 바인딩 초기화
        setContentView(binding.root) // setContentView에 뷰 바인딩의 루트 요소를 전달
        
        viewExample = binding.viewExample  //id도 camelCase로 대체하여 접근
	}
}

XML 레이아웃 파일의 결합 클래스 (ActivityMainBinding)에 포함된 정적 메서드 inflate()를 호출하여 인스턴스를 생성합니다. 해당 인스턴스의 루트 뷰를 setContentView에 전달하여 화면상의 활성 뷰로 만듭니다.

 

실제 사용할 때는 레이아웃 파일의 결합 클래스를 binding으로 받아와 binding.viewExample과 같이 접근하는 것이 용이할 것이다.

 

 

Fragment

 

기존

class MyFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_my, container, false)
    }
}

 

 

 

View Binding 방식

class MyFragment : Fragment() {

    private var _binding: FragmentMyBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentMyBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.textViewMessage.text = "Hello, this is the fragment!"
    }
}

 

Fragment 생명주기와 관련되어 간략하게 언급하면 프래그먼트는 뷰보다 오래 지속되기 때문에 OnDestoryView()에서 

ViewBinding을 해제하지 않으면 메모리 누수의 원인이 됩니다.

 

추가적으로 참조 해제를 위한 _binding 변수는 nullable로 설정하고, 실제 뷰들에 대한 참조를 담고 있는 binding 변수는 non-nullable타입으로 설정하여 안전한(Null-Safe) 패턴을 적용하는 것이 권장됩니다.

 

 

 

 

findViewById와의 차이점

  • Null-Safety      :     뷰 결합은 뷰의 직접 참조를 생성합니다. 유효하지 않은 뷰 ID로 인해 null 포인터 예외가 발생할 위험이 없습니다. 
  • Type-Safety     :    각 바인딩 클래스에 있는 필드의 유형이 XML 파일에서 참조하는 뷰와 일치합니다. 즉, type casting에 있어 예외가 발생할 위험이 없습니다.

 

 


 

 

해당 글은 다음을 참조하여 공부한 내용을 정리한 글입니다.

 

 

뷰 결합  |  Android 개발자  |  Android Developers

 

뷰 결합  |  Android 개발자  |  Android Developers

뷰 결합 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 뷰 결합 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있습니다. 모듈에서 사용 설정

developer.android.com

 

 

+ Recent posts