RecyclerView가 뭐길래?
Android Developers가 정의하는 RecyclerView는 위와 같다.
즉, 대량의 데이터 세트를 동적으로 생성하여 재사용한다는 것이다.
ListView는 notifyDataSetChanged()만 지원한다.
즉, 데이터가 하나라도 변경되면 무조건 새로 layout을 inflate하는 것이다.
❓이것이 얼마나 비효율적인것이길래?
ListView는 스크롤할 때마다 새로운 항목을 로드할 때 layout inflate를 실행한다.
layout inflate는 주로 메인 스레드에서 실행될 뿐만아니라 비동기로 처리되지 않는다.
즉, layout inflate가 실행되면 즉시 layout을 inflate하고, 호출자에게 결과인 뷰를 반환한다.
이 과정에서 해당 스레드가 블록되어 inflate가 완료될 때까지 다음코드를 진행하지 않는 것이다.
실제로 Listview를 만들고 실행시키면 엄청 버…ㅂ.벅인다.
❓그럼 ViewHolder Pattern은 뭐가 다른데?
ViewHolder는 각 뷰 객체를 뷰 홀더에 보관해서 저장해놓고 사용하기 위한 객체이다.
즉, 전에 만들어놨던것을 재사용하겠다는 의미이다.
ViewHolder를 사용하면 layout inflate는 한 번만 실행된다.
onCreateViewHolder
에서 view를 inflate하고 ViewHolder를 생성하여 해당 뷰 객체를 저장한다.
스크롤하면서 화면에 사라지는 뷰는 onBindViewHolder()
를 통해 새 데이터로 업데이트 된다.
이 과정에서 기존의 ViewHolder 객체가 사용되며, 새 데이터와 함께 새로운 내용으로 뷰를 업데이트한다.
즉, 새로운 데이터를 보여주기 위하여 layout inflate가 재실행되지 않는 것이다.
ListView에서도 ViewHoler를 사용할 수 있는거 아닌가요?
맞다. 하지만 Recyclerview는 강제로 사용해야하는 반면 Listview는 선택적이다.
ListView는 ViewHolder 문제 뿐만아니라 횡스크롤을 지원하지 않으며 애니메이션의 적용 또한 불가능하다.
RecyclerView를 구현해보자!
우선 itemView를 만들어야한다.
그냥 간단하게 name, age 두 개의 데이터만 표시하도록 만들었다.
그 후 layout에 recyclerView를 추가한다.
이제 RecyclerView Adapter와 ViewHolder를 구현해줘야한다.
data class Data (
val name: String,
val age: String
)
class MyRecyclerViewAdapter(private val list: List<Data>) :
RecyclerView.Adapter<MyRecyclerViewAdapter.ViewHolder>() {
private val data = list
class ViewHolder(private val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(data: Data) {
val name = data.name
val age = data.age
itemView.apply{
binding.tvName.text = name
binding.tvAge.text = age
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
}
그 후 Activity에 리사이클러뷰를 연결하고 데이터를 넣어주면 된다.
class MyMainActivity : AppCompatActivity() {
private val binding: ActivityMyMainBinding by lazy {
ActivityMyMainBinding.inflate(layoutInflater)
}
private lateinit var myAdapter: MyRecyclerViewAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupRecyclerView()
}
private fun setupRecyclerView() {
val data = settingsData()
myAdapter = MyRecyclerViewAdapter(data)
binding.rvMy.apply {
setHasFixedSize(true)
layoutManager =
LinearLayoutManager(this@MyMainActivity, LinearLayoutManager.VERTICAL, false)
adapter = myAdapter
}
}
private fun settingsData(): List<Data> {
val data = mutableListOf<Data>()
repeat(30) { idx ->
data.add(Data("babo$idx", idx.toString()))
}
return data
}
}
onCreateViewHolder(ViewGroup parent, int viewType)
: viewType 형태의 아이템 뷰를 위한 뷰홀더 객체 생성onBindViewHolder(ViewHolder holder, int position)
: position에 해당하는 데이터를 뷰홀더의 아이템뷰에 표시getItemCount()
: 전체 아이템 개수 리턴
리사이클러뷰를 수직으로 하고 싶어 LinearLayoutManager의 옵션을 VERTICAL로 해주었다.
DiffUtil을 이용하여 효율적으로 데이터를 변경하기
DiffUtil은 androidx 패키지에 포함되어 두 리스트간의 차이를 계산하고, 새로운 리스트로 변경하기 위한 작업목록을 반영하는 것에 도움을 주는 유틸리티 클래스이다.
DiffUtil을 사용하기 위해서는 DiffUtil.ItemCallBack을 구현해야한다.
private val dataDiffCallback = object : DiffUtil.ItemCallback<Data>(){
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem == newItem
}
}
areItemsTheSame
: 두 항목이 같은 객체인지 반환한다.areContentsTheSame
: 두 항목의 데이터가 같은지의 여부를 반환한다.areItemsTheSame
가 true 일 때만 호출된다.
이것을 적용하기 위해서는 위의 RecyclerView.Adapter를 ListAdapter로 변경해주고
DiffUtil.ItemCallback을 Adapter의 생성자로 전달한다.
class MyRecyclerViewAdapter(private val list: List<Data>) :
ListAdapter<Data, MyRecyclerViewAdapter.ViewHolder>(dataDiffCallback) {
private val data = list
class ViewHolder(private val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(data: Data) {
val name = data.name
val age = data.age
itemView.apply {
binding.tvName.text = name
binding.tvAge.text = age
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position])
}
companion object {
private val dataDiffCallback = object : DiffUtil.ItemCallback<Data>() {
override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Data, newItem: Data): Boolean {
return oldItem == newItem
}
}
}
}
그 후 마지막으로 adapter.submitList(list)
로 교체하면 끝이다.
리사이클러뷰를 사용한적은 많이지만 이렇게 DiffUtil을 사용해서 효율적으로 바꾼적은 없었다.
회사에서도 많이 사용한다고 들었는데 나중에 프로젝트하게되면 꼭 사용해보고 싶다…ㅠㅠ
'안찌의 개발일기 > Android' 카테고리의 다른 글
[Android] Compose Preview 심폐소생술!! (0) | 2024.11.19 |
---|---|
[Android] 서버 통신 (2) | 2024.09.25 |
[Android] 코루틴 (Coroutine) (1) | 2024.06.18 |
[Android] AAC ViewModel (0) | 2024.05.31 |
[Android] 뷰 바인딩 (View Binding) (1) | 2024.05.24 |