본문 바로가기
Projects/FineByMe

[Android/Kotlin]FineByMe 프로젝트 리팩토링: RecyclerView.Adapter에서 ListAdapter로의 전환

by quessr 2024. 7. 10.

최근 FineByMe 프로젝트를 리팩토링하면서, RecyclerView.Adapter에서 ListAdapter로 전환하는 과정에서 많은 것을 배웠습니다. 이 글에서는 그 과정을 공유하고, ListAdapter가 왜 더 효율적인지 설명하고자 합니다.

RecyclerView.Adapter에서 DiffUtil을 사용한 구현

기존에 RecyclerView.Adapter를 사용할 때, DiffUtil을 사용하여 데이터 변경 사항을 처리하기 위해 추가적인 코드를 작성해야 했습니다. 아래는 그 예시입니다.

class PhotoAdapter(private val viewModel: BaseViewModel) :
    RecyclerView.Adapter<PhotoAdapter.PhotoViewHolder>() {

    private var photoList: List<Photo> = listOf()
    private var listener: OnPhotoClickListener? = null

    fun setPhoto(newPhotoList: List<Photo>) {
        val diffCallback = PhotoDiffCallback(photoList, newPhotoList)
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        photoList = newPhotoList
        diffResult.dispatchUpdatesTo(this)
    }

    interface OnPhotoClickListener {
        fun onPhotoClick(photo: Photo)
    }

    class PhotoViewHolder(
        private val binding: ItemPhotoBinding,
        private val listener: OnPhotoClickListener?
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(photo: Photo, height: Int) {
            binding.imageViewPhoto.setImageDrawable(null)
            // 뷰의 높이 설정
            binding.imageViewPhoto.layoutParams.height = height

            ImageLoader.loadImage(
                context = binding.imageViewPhoto.context,
                url = photo.thumbUrl,
                imageView = binding.imageViewPhoto
            )

            binding.imageViewPhoto.scaleType = ImageView.ScaleType.CENTER_CROP

            // 사진 클릭 이벤트 설정
            binding.imageViewPhoto.setOnClickListener {
                listener?.onPhotoClick(photo)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoViewHolder {
        val binding = ItemPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return PhotoViewHolder(binding, listener)
    }

    override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
        val height = viewModel.getPhotoHeight(position)
        val scale = holder.itemView.context.resources.displayMetrics.density
        val heightInPx = (height * scale + 0.5f).toInt()
        holder.bind(photoList[position], heightInPx)
    }

    override fun getItemCount(): Int = photoList.size

    fun setOnPhotoClickListener(listener: OnPhotoClickListener) {
        this.listener = listener
    }

    fun clearData() {
        val diffCallback = PhotoDiffCallback(photoList, listOf())
        val diffResult = DiffUtil.calculateDiff(diffCallback)
        photoList = listOf()
        diffResult.dispatchUpdatesTo(this)
    }
}

class PhotoDiffCallback(
    private val oldList: List<Photo>,
    private val newList: List<Photo>
) : DiffUtil.Callback() {

    override fun getOldListSize(): Int {
        return oldList.size
    }

    override fun getNewListSize(): Int {
        return newList.size
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition].id == newList[newItemPosition].id
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        return oldList[oldItemPosition] == newList[newItemPosition]
    }
}

 

위 코드는 데이터를 업데이트하기 위해 setPhoto 메서드와 DiffUtil.Callback 클래스를 사용합니다. 이로 인해 코드가 장황해지고 유지보수가 어려워집니다.

ListAdapter로 전환 후의 구현

ListAdapter를 사용하면, DiffUtil을 내부적으로 처리하여 코드가 훨씬 간결해집니다. 아래는 ListAdapter를 사용한 예시입니다.

class PhotoAdapter(private val viewModel: BaseViewModel) :
    ListAdapter<Photo, PhotoAdapter.PhotoViewHolder>(diffUtil) {

    private var listener: OnPhotoClickListener? = null

    interface OnPhotoClickListener {
        fun onPhotoClick(photo: Photo)
    }

    class PhotoViewHolder(
        private val binding: ItemPhotoBinding,
        private val listener: OnPhotoClickListener?
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(photo: Photo, height: Int) {
            binding.imageViewPhoto.setImageDrawable(null)
            binding.imageViewPhoto.layoutParams.height = height

            ImageLoader.loadImage(
                context = binding.imageViewPhoto.context,
                url = photo.thumbUrl,
                imageView = binding.imageViewPhoto
            )

            binding.imageViewPhoto.scaleType = ImageView.ScaleType.CENTER_CROP

            binding.imageViewPhoto.setOnClickListener {
                listener?.onPhotoClick(photo)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoViewHolder {
        val binding = ItemPhotoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return PhotoViewHolder(binding, listener)
    }

    override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
        val height = viewModel.getPhotoHeight(position)
        val scale = holder.itemView.context.resources.displayMetrics.density
        val heightInPx = (height * scale + 0.5f).toInt()
        holder.bind(getItem(position), heightInPx)
    }

    fun setOnPhotoClickListener(listener: OnPhotoClickListener) {
        this.listener = listener
    }

    fun clearData() {
        submitList(emptyList())
    }

    companion object {
        private val diffUtil = object : DiffUtil.ItemCallback<Photo>() {
            override fun areItemsTheSame(oldItem: Photo, newItem: Photo): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: Photo, newItem: Photo): Boolean {
                return oldItem == newItem
            }
        }
    }
}

변경 사항

  1. setPhoto 메서드 삭제: ListAdapter는 내부적으로 DiffUtil을 처리하므로 setPhoto 메서드를 사용할 필요가 없습니다. 대신 submitList 메서드를 사용하여 데이터를 업데이트합니다.
  2. DiffUtil.Callback 클래스 간소화: DiffUtil.Callback 클래스를 companion object로 이동하여 코드가 더 간결해졌습니다.
  3. getItemCount 메서드 제거: ListAdapter는 getItemCount 메서드를 자동으로 제공하므로, 이를 직접 구현할 필요가 없습니다.
  4. 데이터 업데이트 간소화: 데이터를 업데이트할 때, submitList 메서드를 사용하여 간단하게 데이터를 설정할 수 있습니다.

데이터 업데이트 방법

기존 방식:

favoriteListViewModel.photos.observe(
    viewLifecycleOwner
) { photos ->
    photoAdapter.setPhoto(photos)
    binding.tvEmpty.isVisible = photos.isEmpty()
}

 

변경 후:

favoriteListViewModel.photos.observe(
    viewLifecycleOwner
) { photos ->
    photoAdapter.submitList(photos)
    binding.tvEmpty.isVisible = photos.isEmpty()
}

결론

RecyclerView.Adapter에 DiffUtil을 사용해도 되지만, ListAdapter를 사용하는 것이 더 효율적입니다. ListAdapter는 코드의 간결성, 효율성, 유지보수 용이성 등 여러 장점을 제공합니다. 데이터 업데이트 시 내장된 submitList 메서드를 사용하면 더욱 직관적이고 간단하게 데이터를 처리할 수 있습니다.

 


https://github.com/quessr/fine-by-me 

 

GitHub - quessr/fine-by-me: - 사용자가 즐겨찾기한 사진을 저장하고 관리할 수 있는 안드로이드 애플리

- 사용자가 즐겨찾기한 사진을 저장하고 관리할 수 있는 안드로이드 애플리케이션. Contribute to quessr/fine-by-me development by creating an account on GitHub.

github.com