기존 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