RecyclerView 사용할 때 주의할 점

홍성덕·2024년 7월 11일

RecyclerView를 사용할 때 주의할 점에 대해 고민해보았다.

class ImagesAdapter(
    private val dataSet: List<Document>,
    private val onClickFavorite: (data: Document, position: Int) -> Unit
) : RecyclerView.Adapter<ImagesAdapter.ViewHolder>() {

//...

class SearchFragment : BaseFragment(R.layout.fragment_search) {

    private var binding: FragmentSearchBinding? = null

    private lateinit var adapter: ImagesAdapter

    // ...

    private fun getSearchedImages(keyword: String) {
    	// ...
        val service = retrofit.create(SearchImageService::class.java)
        service.getSearchedImages(BuildConfig.REST_API_KEY, keyword)
            .enqueue(object : Callback<GetSearchedImagesResponse> {
                override fun onResponse(
                    call: Call<GetSearchedImagesResponse>,
                    response: Response<GetSearchedImagesResponse>
                ) {
                    val dataset = response.body()!!.documents
                    thread {
                        val dao = DocumentDatabase.getDatabase(requireContext()).documentDao()
                        val favorites = dao.getAll()

                        dataset.forEach { document ->
                            if (favorites.any { it.thumbnailUrl == document.thumbnailUrl }) {
                                document.isFavorite = true
                            }
                        }

                        activity?.runOnUiThread {
                            adapter = ImagesAdapter(dataset, ::onClickFavorite)
                            binding?.recyclerViewImageList?.layoutManager =
                                LinearLayoutManager(context)
                            binding?.recyclerViewImageList?.adapter = adapter
                        }
                    }
                }

                // ...

            })
    }

    // ...
}

먼저 위의 코드는 내가 샘플 프로젝트를 진행하면서 작성한 코드 중 일부이다.

위의 코드에서 몇 가지 문제점이 존재한다.

1. 데이터를 업데이트할 때마다 매번 새로운 ImagesAdapter 객체를 생성

ImagesAdapter 클래스에 생성자로 dataSet이라는 List를 넘겨주었다. 만약 ImagesAdapter의 데이터가 변경되지 않는다면 생성자로 dataSet을 넣어주는 것은 문제가 없다. 하지만 나는 SearchFragment라는 검색 기능이 있는 화면에서, 검색할 때마다 API 통신을 진행하여, 매번 새로운 dataSet으로 업데이트를 해주어야 했다.

그래서 이렇게 생성자로 dataSet을 넣어주게 되면 검색하여 데이터를 업데이트할 때마다 ImagesAdapter 객체를 매번 새로 생성해야 하는 불필요한 작업이 발생한다. 그래서 ImgesAdapter에 dataSet 생성자 파라미터를 제거하고 setData() 함수를 작성하여 데이터만 업데이트하도록 수정했다.


2. 데이터를 업데이트할 때마다 매번 setLayoutManager(), setAdapter()를 호출

데이터를 업데이트할 때마다 매번 setLayoutManager(), setAdapter()를 호출하고 있다. 그런데 이 과정은 View를 초기화하는 과정이기 때문에 한번만 하면 된다. 이렇게 매번 해줄 필요가 없는 작업이다. View를 이미 초기화해놓고, 그 안에 들어가는 dataSet만 업데이트하여 화면만 다시 그려주면 된다.

그리고 API 통신을 하고 나서 setAdapter()를 호출하고 있기 때문에 앱에서 No adapter attached; skipping layout 오류가 발생하는 추가적인 문제도 있었다.

그래서 이 과정을 onViewCreated()로 옮겨서 매번 set함수를 호출하지 않고 올바른 시점에 한번만 호출하도록 수정하였다.


3. ViewHolder class를 inner 키워드로 선언

ViewHolder class를 선언할 때 ImagesAdapter 안에 inner 키워드를 이용해서 class를 선언하였다. ImagesAdapter class의 멤버인 onClickFavorite()을 호출하기 위해서이다. inner 키워드를 사용하면 Outer class의 멤버에 접근할 수 있다.

사실 이 방법이 지금 코드에서 작동하는데는 큰 문제가 없다. 하지만 개발자의 실수로, 접근하면 안되는 Outer class의 멤버에 접근하게 될 수도 있다. 지금은 작은 프로젝트라서 이런 부분에서 실수가 발생하지 않지만, 프로젝트가 커질수록 잘못된 코드를 작성할 위험성이 늘어난다. 그래서 ViewHolder를 별도의 class 파일로 분리하였다.


수정 후 코드

class ImagesAdapter(
    private val onClickFavorite: (data: Document, position: Int) -> Unit
) : RecyclerView.Adapter<ItemSearchedViewHolder>() {

    private var dataSet: List<Document> = listOf()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemSearchedViewHolder {
        val binding =
            ItemSearchedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ItemSearchedViewHolder(binding, onClickFavorite)
    }

    override fun onBindViewHolder(holder: ItemSearchedViewHolder, position: Int) {
        holder.bind(dataSet[position])
    }

    override fun getItemCount() = dataSet.size

    fun setData(dataSet: List<Document>) {
        this.dataSet = dataSet
        notifyDataSetChanged()
    }
}

class ItemSearchedViewHolder(
    private val binding: ItemSearchedBinding,
    private val onClickFavorite: (data: Document, position: Int) -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    fun bind(data: Document) {
        Glide.with(binding.imageViewThumbnail.context)
            .load(data.thumbnailUrl)
            .into(binding.imageViewThumbnail)
        binding.textViewSiteName.text = data.displaySitename
        if (data.isFavorite) {
            Glide.with(binding.imageViewFavorite.context)
                .load(android.R.drawable.btn_star_big_on)
                .into(binding.imageViewFavorite)
        }

        binding.imageViewFavorite.setOnClickListener {
            onClickFavorite(data, adapterPosition)
        }
    }
}

class SearchFragment : BaseFragment(R.layout.fragment_search) {

    private var binding: FragmentSearchBinding? = null
    private val adapter = ImagesAdapter(::onClickFavorite)

    //...

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

        //...

        binding?.recyclerViewImageList?.layoutManager = LinearLayoutManager(context)
        binding?.recyclerViewImageList?.adapter = adapter
    }

    //...

    private fun getSearchedImages(keyword: String) {
    	//...
        val service = retrofit.create(SearchImageService::class.java)
        service.getSearchedImages(BuildConfig.REST_API_KEY, keyword)
            .enqueue(object : Callback<GetSearchedImagesResponse> {
                override fun onResponse(
                    call: Call<GetSearchedImagesResponse>,
                    response: Response<GetSearchedImagesResponse>
                ) {
                    val dataset = response.body()!!.documents
                    thread {
                        val dao = DocumentDatabase.getDatabase(requireContext()).documentDao()
                        val favorites = dao.getAll()

                        dataset.forEach { document ->
                            if (favorites.any { it.thumbnailUrl == document.thumbnailUrl }) {
                                document.isFavorite = true
                            }
                        }

                        activity?.runOnUiThread {
                            adapter.setData(dataset)
                        }
                    }
                }

                //...

            })
    }

    //...
}
profile
안드로이드 주니어 개발자

0개의 댓글