ListAdapter 를 적용하여 RecyclerView 를 구현해보자

순순·2024년 10월 30일

Flutter

목록 보기
7/16

이전 포스팅의 방식대로 recyclerView 를 구현할 때, 데이터가 변경되면 반드시 아래와 같은 notify~ 메서드를 호출해 데이터 변경을 알려주어야 했다. 매번 갱신 코드를 써야하는 것이 굉장히 번거롭다고 생각하던 중 diffUtil 을 적용하는 ListAdapter 에 대해 알게 되어서 이를 사용해보고자 한다.

notifyItemChanged(int)
notifyItemInserted(int)
notifyItemRemoved(int)
notifyItemRangeChanged(int, int)
notifyItemRangeInserted(int, int)
notifyItemRangeRemoved(int, int)

우선 간단하게 ListAdapter 와 DiffUtil 이 뭔지 그 개념을 정리해보았다.


ListAdapter


  • RecyclerView.Adapter 를 확장한 클래스
  • DiffUtil 을 사용하여 리스트 데이터 변경 시 최적화된 UI 갱신 제공
  • 백그라운드 스레드에서 데이터를 비교한다.

DiffUtil


개념

  • 두 데이터셋을 받아서 그 차이를 계산해주는 클래스
  • 그중 변동 있는 부분만 파악하여 recyclerView 를 갱신할 수 있다.
  • flutter 의 equtable과 유사

사용 흐름

  • DiffUtil.Callback을 상속받는다
  • areItemTheSame 메서드로 동일한 아이템인지 확인한다.
  • 동일한 아이템일 경우 areContentsTheSame 메서드로 데이터 내용이 같은지 확인한다.
  • recyclerView의 어댑터 내부에 diffUtill 객체를 만들어준다
  • diffUtil객체.currentList 로 데이터 참조
  • diffUtil객체.sumbitList 로 데이터 갱신

클래스

AsyncListDiffer

  • DiffUtil을 백그라운드 스레드에서 실행하도록 도와주는 클래스.
  • 비교할 데이터 수가 많을 경우, 연산이 길어지기 때문에 백그라운드 스레드에서 처리되어야 한다. 이를 수행해주는 것이 AsyncListDiffer

ListAdapter

  • AsyncListDiffer를 더 쓰기 쉽게 랩핑한 클래스
  • 사용법은 RecyclerView 의 Adapter 를 만들 때 ListAdapter 를 상속받도록 하면 된다.

구현 코드

  • ListAdapter<데이터클래스, 뷰홀더>(DiffiCallback)
  • StudioDiffCallback은 DiffUtil 콜백 객체로, 리스트의 변경 사항을 감지하는 역할을 한다.
  • areItemsTheSame 로 두 아이템이 같은지 비교하고, areContentsTheSame 를 통해 아이템의 내용이 같은지 비교한다.
package com.example.presentation.main.view.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.example.domain.model.StudioInfoWithConcept
import com.example.presentation.databinding.ItemResultViewBinding

class ResultViewAdapter : ListAdapter<StudioInfoWithConcept, ResultViewHolder>(StudioDiffCallback) {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ResultViewHolder {
        return ResultViewHolder(
            ItemResultViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        )
    }

    override fun onBindViewHolder(holder: ResultViewHolder, position: Int) {
        val studio = getItem(position)
        holder.bind(studio)
    }

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

            override fun areContentsTheSame(oldItem: StudioInfoWithConcept, newItem: StudioInfoWithConcept): Boolean {
                return oldItem == newItem
            }
        }
    }
}
  • viewHolder
package com.example.presentation.main.view.adapter

import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.example.domain.model.StudioInfoWithConcept
import com.example.presentation.databinding.ItemResultViewBinding
import java.util.Locale

class ResultViewHolder(
    private val binding: ItemResultViewBinding
) : RecyclerView.ViewHolder(binding.root) {
    private val portfolioAdapter = PortfolioAdapter()

    fun bind(studio: StudioInfoWithConcept) {
        val id = studio.id
        val name = studio.name
        val rating = studio.rating
        val profilePrice = studio.profilePrice
        val portfolioUrls = studio.portfolioUrls
        val profileURL = studio.profileURL

        binding.tvStudioName.text = name
        binding.tvRating.text = String.format(Locale.US, "%.1f", rating.toDouble())
        imageBind(profileURL)
    }

    fun imageBind(imageUrl: String) {
        Glide.with(binding.root)
            .load(imageUrl)
            .into(binding.ivStudioMainImage)
    }
}
  • Fragment 에서 뷰 바인딩 및 어댑터 연결
class ResultViewFragment : Fragment(R.layout.fragment_result_view) {
    private val viewModel: ResultViewModel by viewModels()
    private val sharedViewModel: HomeConceptViewModel by activityViewModels()
    private val args: ResultViewFragmentArgs by navArgs()
    private lateinit var resultViewAdapter: ResultViewAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val binding = FragmentResultViewBinding.bind(view)

        with(binding){
            toolbar.setNavigationOnClickListener {
                sharedViewModel.onRequestBackPress()
            }
        }

        viewModel.getInitializedStudio(args.conceptId)
        setupRvStudioList(binding)
        observeResultViewModel()
        observeFilterState()
    }

    private fun setupRvStudioList(binding: FragmentResultViewBinding) {
        resultViewAdapter = ResultViewAdapter()

        binding.rvStudioList.apply {
            layoutManager =
                LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
            itemAnimator = null
            addItemDecoration(
                DividerItemDecoration(
                    requireContext(),
                    DividerItemDecoration.VERTICAL
                )
            )
            adapter = resultViewAdapter
        }


기타


diffUtil 이 백그라운드 스레드에서 데이터를 비교한다기에 백그라운드 스레드가 뭔지 찾아봤다. 메모 메모.


백그라운드 스레드

  • 앱의 UI와 상호작용 하지 않는 작업을 수행한다.

스레드

  • 안드로이드에서 앱은 기본적으로 MainThread 에서 실행된다.
  • MainThread: 주로 UI 업데이트 및 사용자 입력처리 등을 담당한다
  • 데이터 처리나 네트워크 요청 등 시간이 오래걸릴 수 있는 작업을 MainThread 에서 실행하면 앱이 느려지거나 멈춘 것처럼 보일 수 있다.
  • 따라서 이런 작업들은 UI에 영향을 미치지 않는 백그라운드 쓰레드에서 작업해야 한다.
  • 백그라운드 스레드로는 코틀린 코루틴을 많이 쓴다.
profile
플러터와 안드로이드를 공부합니다

0개의 댓글