본문으로 바로가기

RecyclerView.Adapter -> ListAdapter 로 전환

category UniteWiki/Frontend 2022. 4. 15. 23:10

기존 RecyclerView.Adapter 사용은 다음과 같습니다.

class PokemonRankAdapter (var list: ArrayList<PokemonRankData>):
    RecyclerView.Adapter<PokemonRankAdapter.ViewHolder>(){
    
 inner class ViewHolder(val binding:ItemPokemonrankingBinding): RecyclerView.ViewHolder(binding.root){
		//...        
}
    
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {       val binding = ItemPokemonrankingBinding.inflate(LayoutInflater.from(parent.context),parent,false)
        return ViewHolder(binding)
    }

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

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

list 변수를 생성자로 받아서 adpater로 넘겨준 뒤에 해당 adapter를 아래의 예시와 같이 View에서 View의 RecyclerView에 적용시키게 됩니다.

 

@AndroidEntryPoint
class BalancePokemonFragment : Fragment() {

private var adapter: PokemonRankAdapter = PokemonRankAdapter(ArrayList<PokemonRankData>())

//..

private fun subscribeUi() {
    viewmodel.getdata("AttackPokemonRanking")

    viewmodel.pokemondatalist.observe(viewLifecycleOwner) { result ->

        adapter = PokemonRankAdapter(result)
        binding.attackRecyclerview.adapter = adapter
        binding.attackRecyclerview.layoutManager = LinearLayoutManager(activity)
        adapter.notifyDataSetChanged()
    }
}
//..
}

list에 변화가 있을 경우 어댑터는 notifyDataSetChanged() 메소드를 호출하여 리스트를 갱신하게 됩니다.

notifyDataSetChanged() 메소드의 의 문제는 리스트 전체를 다시 재적용 하면서 어댑터 내의 onCreateView() 부터 onBindViewHolder() 까지 호출하게 되기 때문에 아이템이 하나만 추가되었거나 변경되었을 때도 이 메소드를 사용하게 되면 리스트가 큰 경우 불필요한 메모리 소모를 유발하게 됩니다.

 

물론 notifyItemChanged() , notifyItemRemoved() 메소드 등을 활용할 수도 있으나 이는 활용하기 상당히 번거롭습니다.

 

그래서 등장한게 ListAdapter 입니다.

ListAdapter은 DiffUtil 을 활용하여 리스트 내 아이템에 변화가 있는지 없는지만 확인 후 변화가 있는 아이템에만 처리를 해주기 때문에, notifyDataSetChanged()보다 메모리를 훨씬 효율적으로 사용하게 됩니다.

 

아래는 ListAdapter 사용 예시입니다.

class PokemonRankAdapter:
    ListAdapter<PokemonRankData,PokemonRankAdapter.ViewHolder>(diffUtil){

    inner class ViewHolder(val binding:ItemPokemonrankingBinding): RecyclerView.ViewHolder(binding.root){
		//..
    }

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

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

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


    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<PokemonRankData>() {
            override fun areContentsTheSame(oldItem: PokemonRankData, newItem: PokemonRankData) =
                oldItem == newItem

            override fun areItemsTheSame(oldItem: PokemonRankData, newItem: PokemonRankData) =
                oldItem.pokemon_name == newItem.pokemon_name
        }
    }
}

 

ListAdapter 클래스는 기존 아이템과 예전 아이템을 비교해주는 DiffUtil을 생성자로 받습니다.

list 자체는 submitList() 메소드를 통해 적용하기 때문에 list 자체를 생성자로 받지는 않습니다.

 

위 어댑터에서 onBindViewHolder 와 getItemCount 는 생략해도 되지만 현재 적용중인 리스트를 currentList 로 접근할 수 있으며 혹여나 해당 메소드를 Override 해야할 경우 기존 Adapter와 동일하게 Override 할 수 있음을 알려주기 위해 작성했습니다.

 

위와 같이 어댑터를 작성 후 View 에서는 다음과 같이 적용합니다.

 

@AndroidEntryPoint
class AttackPokemonFragment : Fragment() {

    private lateinit var binding: FragmentAttackPokemonBinding
    private val viewmodel:PokemonRankViewModel by viewModels()
    private lateinit var adapter: PokemonRankAdapter

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?,
    ): View? {
        binding = FragmentAttackPokemonBinding.inflate(inflater,container,false)
        binding.apply {
            attackRecyclerview.adapter = adapter
            attackRecyclerview.layoutManager = LinearLayoutManager(activity)
        }

        runBlocking {
            launch {
                subscribeUi()
            }.join()
        }
        return binding.root
    }

    private fun subscribeUi() {
        viewmodel.getRanks(Constants.ATTACK_RANKING)
        viewmodel.ranklist.observe(viewLifecycleOwner){res->
            when(res){
                is Response.Loading -> { print("Loading...") }
                is Response.Success -> {
                    adapter.submitList(res.data)
                    binding.loadComplete = !(res.data.isNullOrEmpty())
                }
                is Response.Failure -> { print("Failure...") }
            }
        }
    }

}

submitList() 메소드를 사용하여 리스트를 어댑터에 제출하게 됩니다.

리스트를 제출받은 ListAdapter 은 DiffUtil 을 통해 기존 아이템과 변화가 있는 부분을 체크하게 되고, 변화가 있는 부분만을 처리해 기존 RecyclerView.Adapter에서 notifyDataSetChanged() 메소드를 호출하는 것 보다 훨씬 더 효율적으로 메모리를 사용합니다.

 

 

*** 04.26 / 추가

메모리 효율성으로 인해 Paging + Flow 를 활용하여 대용량 데이터를 순차적으로 필요한 만큼만 캐싱해야 될 경우 

PagingDataAdapter를 사용합니다.

 

대부분의 사용법이 일치하지만 주로 사용하는 다음 메소드가 다릅니다.

//how to get current List that applied to Adapter
ListAdapter ->
currentList 
PagingDataAdapter ->
getItem

//how to submit data to adapter
ListAdapter ->
submitList
PagingDataAdapter ->
submitData