지금까지 제가 리사이클러뷰를 사용할 때의 Work Flow는 아래와 같았습니다.리사이클러뷰에 표시될 아이템뷰를 데이터 리스트로부터 생성하는 역할을 담당합니다.
1. Activity 또는 Fragment에 RecyclerView를 추가
2. RecyclerView.Adapter를 상속받아 데이터 목록을 Item 단위의 VIew로 구성하여 화면에 표시하기 위한 Apdater를 구현
3. RecyclerView.ViewHolder를 상속받아 개별 데이터에 대응하는 ViewHolder를 구현
4. setAdapter()와 setLayoutManager()를 이용해, Adapter, LayoutManager를 지정
위와 같이 사용해오다, RecyclerView의 Anti Pettern에 대한 글을 보고 여러 번 읽어보고, 직접 구현해서 Log를 찍어보며, 흐름에 대해 파악해보며 공부해봤습니다.
※ 순서는 다음과 같으며, 순서 1, 2는 생략하겠습니다.
- Fragment에 RecyclerView 추가 (fragment_***.xml에 layoutManager 설정)
- Item View Layout 생성
- BaseModel 생성
- AdapterListener와 이를 확장하는 TodoListener 생성
- BaseViewHolder 생성
- BaseViewHolder를 상속받는 TodoViewHolder오 EmptyViewHolder 생성
- BaseAdapter 생성
- Fragment에서 BaseAdapter Instance 생성과 RecyclerView와 연결
3. BaseModel 생성
BaseAdapter와 BaseViewHolder에서 공통으로 쓰일 BaseModel 입니다.
아래 소스코드를 보면 알 수 있듯이, BaseAdapter를 Marker Interface인 Serializable을 통해 시스템에 내부 변수를 통해 직렬화하여 객체를 생성하도록 알리게 했고, ListAdapter를 상속받아 사용하기위해 DiffUtil.ItemCallback()을 구현했습니다. (이 후에 Paging Component까지 공부해서 정리할 예정인데, ItemCallback()을 구현해야 한다고 합니다.)
**Marker Interface : 일반적인 인터페이스와 동일하지만, 사실상 아무 메소드도 선언하지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가진다는 것을 표시하는 인터페이스
**DiffUtil : 리사이클러뷰의 성능을 개선할 수 있게 해주는 유틸리티 클래스로, 기존의 List와 교체할 List를 비교해 실질적으로 업데이트 해야할 목록만을 반환하여, Adapter에 대한 업데이트를 알리는데 사용됩니다.
RecyclerView.Adapter를 상속받아 Adapter 클래스를 구현한다면, 4가지 추상 메서드를 overriding 해야합니다.
- areItemsTheSame( oldPosition:Int , newPosition:Int) : 두 객체가 동일한 항목을 나타내는지 확인합니다.
- getOldListSize() : 바뀌 기 전 리스트의 크기를 리턴합니다.
- getNewListSize() : 바뀐 후 리스트의 크기를 리턴합니다.
- areContentsTheSame( oldPosition:Int, newPosition:Int) : 두 항목의 데이터가 같은지 확인합니다.
이 메서드는 areItemsTheSame 이 true일 때만 불립니다.
https://onemask514.tistory.com/48
RecycerView.Adapter를 사용할 때 AsyncListDiffer를 사용하려면, DiffUtil.ItemCallback()을 구현하고, AsyncListDiffer를 인스턴스를 생성하여, onBindViewHolder(), getItemCount(), submitList() 등에 활용하면 되지만, ListAdapter를 사용하면 내부적으로 사용하고 있어, ListAdapter를 상속받는 Adapter를 생성하는 것만으로 AsyncListDiffer를 사용할 수 있습니다.
https://s2choco.tistory.com/33
추가적으로 AsyncListDiffer에서 넘어노는 currentList는 READ ONLY List로 변경이 불가능하니, 리사이클러뷰의 데이터를 변경하고 싶은 경우엔 submitList()를 이용해야만 합니다.
notifyDataSetChanged()를 호출하면, Adapter에게 리사이클러뷰의 리스트 데이터가 변경되었음을 알리고, 모든 항목을 업데이트 하도록 합니다. 1000개의 데이터 중 1개만 바뀐 경우 position을 안다면 다행이지만, 아니라면 1000개 전부를 업데이트하게 됩니다. 이런 불필요한 교체 비용을 줄이기 위해 고안된 것이 DiffUtil 입니다.
/**
* DiffUtil : RecyclerView의 성능을 개선할 수 있게 해주는 유틸리티 클래스, 기존의 List와 교체할,
* List를 비교해 실질적으로 업데이트가 필요한 Item들을 필터링함
*/
abstract class BaseModel (
open val id : Long?
) : Serializable {
init {
Log.d(Constants.TAG, "BaseModel called")
}
companion object {
/**
* 1. areItemsTheSame() : oldItem, newItem이 동일한 Item인지 체크, Item의 고유식별자를 이용하면 됨
*
* 2. areContesTheSame() : oldItem, newItem이 동일한 내용물을 가졌는지 체크, areItemsTheSame()가 true이면 호출됨
*/
val DIFF_CALLBACK : DiffUtil.ItemCallback<BaseModel> = object : DiffUtil.ItemCallback<BaseModel>() {
override fun areItemsTheSame(oldItem: BaseModel, newItem: BaseModel): Boolean {
Log.d(Constants.TAG, "BaseModel areItemsTheSame() called : ${oldItem.id}")
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: BaseModel, newItem: BaseModel): Boolean {
Log.d(Constants.TAG, "BaseModel areContentsTheSame() called")
return oldItem === newItem
}
}
}
}
4. AdapterListener와 이를 확장하는 TodoListener 생성
사용자 이벤트 발생 시 BsaeAdapter에 전달된 Listener가 무엇인지를 구별할 수 있어야 하기에 아래와 같이 Listener Interface를 생성했습니다.
interface AdapterListener {
}
interface TodoListener : AdapterListener {
fun onClickItem(position: Int, model: BaseModel)
}
5. BaseViewHolder 생성
ViewHolder는 각 View를 보관하는 Holder 객체로, RecyclerView는 inflate를 최소화하기 위해서 View를 재활용 합니다. 이 때 각 View의 내용을 업데이트 하려면 findViewById()를 매번 호출하면서, 성능저하가 발생하지만 ItemView의 각 요소를 바로 bind 할 수 있도록 저장해두고 사용해 성능저하를 줄이는 패턴입니다.
RecyclerView.ViewHolder를 상속받아 사용하기 위해선 위에서 언급한 ItemView를 전달해야하며, ItemView는 layout을 View 객체로 만든 것을 의미합니다. 이 작업은 BaseAdapter의 onCreateViewHolder()에서 할 것이고, 예제에선 해당 View 객체를 DataBinding으로 전달받아, xml 전체를 감싸는 최상단의 부모를 의미하는 root라는 property를 사용하여 View를 RecyclerView.ViewHolder에 전달합니다.
소스코드를 보면 Class 명 우측에 <M : BaseModel>을 볼 수 있습니다. 이 것을 Generic이라고 하며, 제네릭 클래스로 선언하기 위해선, 사용될 type parameter를 정의해야 합니다. 예제에선 M의 타입이 BaseModel인 type parameter 입니다.
abstract class BaseViewHolder<M : BaseModel> (
binding : ViewDataBinding
) : RecyclerView.ViewHolder(binding.root) {
/**
* View에 bind 작업을 하는 중, 이전의 data가 남아있을 수 있어 초기화시키는 작업
*/
open fun reset() {}
/**
* model을 전달받아 View에 bind하는 작업
*/
abstract fun bindData(model : M)
}
6. BaseViewHolder를 상속받는 TodoViewHolder와 EmptyViewHolder 생성
위에서 BaseViewHolder에 대한 설명을 해두었기 때문에, 간단하게 설명하도록 하겠습니다.
ViewDataBinding인 LayoutToodoItemBinding과 고차함수인 onItemClick을 받습니다. 그리고 사용자의 클릭 이벤트가 발생하면 binding.root.setOnClickListener에 의해 position을 담은 onItemClick()이 반환됩니다.
EmptyViewHolder에 대한 설명은 7.에서 하겠습니다.
class TodoViewHolder(
private val binding: LayoutTodoItemBinding,
private val onItemClick: (position: Int) -> Unit
) : BaseViewHolder<TodoModel>(binding){
private val THIS_NAME = this::class.simpleName
override fun bindData(model: TodoModel) {
binding.todoItem = model
}
init {
Log.d(Constants.TAG, "$THIS_NAME, init() called ")
binding.root.setOnClickListener {
Log.d(Constants.TAG, "$THIS_NAME, setOnClickListener() called : 사용자 이벤트가 발생했습니다.")
onItemClick(adapterPosition)
}
}
}
class EmptyViewHolder(
private val binding: LayoutEmptyItemBinding
) : BaseViewHolder<BaseModel>(binding) {
override fun bindData(model: BaseModel) = Unit
}
7. BaseAdapter 생성
Adapter는 리사이클러뷰에 표시될 아이템뷰를 데이터 리스트로부터 생성하는 역할을 담당합니다. ListAdapter를 사용하기위해 onCreateViewHolder(), onBindViewHolder(), getItemCount() 함수를 overriding 해야합니다.
getItemCount() : 전체 Item의 개수를 반환해줍니다.
onCreateViewHolder() : viewType에 따른 ViewHolder를 생성하고, View 객체 즉, 아이템 하나가 디자인된 Layout을 반환합니다.
onBindViewHolder() : ViewHolder에 데이터들을 bind 해줍니다.
**overriding : 상속 관계에 있는 클래스 간에 상위 클래스의 메서드를 재정의하는 것을 의미합니다.
@Suppress("UNCHECKED_CAST")
class BaseAdapter<M : BaseModel>(
private var modelList : List<BaseModel>,
private val adapterListener : AdapterListener
) : ListAdapter<BaseModel, BaseViewHolder<M>>(BaseModel.DIFF_CALLBACK){
private val THIS_NAME = this::class.simpleName
override fun getItemCount(): Int = modelList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<M> {
val inflater = LayoutInflater.from(parent.context)
return if (modelList.checkType<TodoModel>()){
Log.d(Constants.TAG, "$THIS_NAME, onCreateViewHolder() called : ViewHolder가 생성됩니다.")
TodoViewHolder(
binding = LayoutTodoItemBinding.inflate(inflater, parent, false),
onItemClick = { adapterPosition ->
Log.d(Constants.TAG, "$THIS_NAME, onItemClick() called : ViHolder로부터 응답을 받았습니다.")
if (adapterListener is TodoListener) {
Log.d(Constants.TAG, "$THIS_NAME, onItemClick() called : 구현체에 값을 전달합니다.")
adapterListener.onClickItem(adapterPosition, modelList[adapterPosition])
}
}) as BaseViewHolder<M>
}
else {
Log.d(Constants.TAG, "$THIS_NAME, onCreateViewHolder() called : ViewHolder가 생성됩니다.")
EmptyViewHolder(LayoutEmptyItemBinding.inflate(inflater, parent, false)) as BaseViewHolder<M>
}
}
override fun onBindViewHolder(holder: BaseViewHolder<M>, position: Int) {
Log.d(Constants.TAG, "$THIS_NAME, onBindViewHolder() called : ViewHolder에 bind 합니다.")
holder.bindData(modelList[position] as M)
}
override fun submitList(list: List<BaseModel>?) {
list?.let {
modelList = it
super.submitList(list)
}
}
override fun onViewRecycled(holder: BaseViewHolder<M>) {
Log.d(Constants.TAG, "$THIS_NAME, onViewRecycled() called : ViewHolder가 재사용 됩니다.")
super.onViewRecycled(holder)
}
}
file 명 : Extensions.kt
/**
* 1. 일반적인 Generic Function Body에서 타입 T는 런타임에는 Type erasure 때문에 접근이 불가능
* 하지만 reified 타입 파라미터와 함께 inline 함수를 만들면, 런타임에 타입 T에 접근이 가능하며,
* "변수 is T"를 통해 "변수"가 T의 인스턴스인지 검사할 수 있음
*
* 2. Type erasure : Generic은 컴파일 시간에 엄격한 Type 검사를 제공하지만,
* Generic을 구현하기 위해 Java 컴파일러는 Type erasure를 적용합니다
*/
inline fun <reified T> List<BaseModel>.checkType() =
all { it is T }
8. Fragment에서 BaseAdapter Instance 생성과 RecyclerView와 연결
private val adapter by lazy {
BaseAdapter<TodoModel>(
modelList = listOf(),
adapterListener = object : TodoListener {
override fun onClickItem(position: Int, model: BaseModel) {
Log.d(Constants.TAG, "$THIS_NAME onClickItem() : $position, $model")
val bundle = Bundle()
bundle.putSerializable("TodoItemData", model)
/*
Intent(requireContext(), ***********::class.java).apply {
putExtra("bundle", bundle)
startActivity(this, getBudle(v))
}
*/
}
})
}
private fun getBudle(v : View) : Bundle {
// API 21 이상부터 가능하지만, 최소 버전이 23이라 분기처리 없이 처리
return ActivityOptions.makeSceneTransitionAnimation(
requireActivity(),
Pair.create(v, resources.getString(R.string.title_transition_name))
).toBundle()
}
override fun onViewCreate() {
binding.todoCategoryRecyclerView.adapter = adapter
adapter.submitList(listOf(
TodoModel( ... ),
TodoModel( ... ),
TodoModel( ... ),
TodoModel( ... )
))
}
'Android' 카테고리의 다른 글
👋Android Programming : TextInputLayout을 이용한 UI 디자인 (0) | 2022.03.21 |
---|---|
Retrofit2, OkHttpInterceptor, Koin, RxKotlin, Coroutine, Sandwich 등을 이용해 내가 작성한 네트워크 통신 방법 (0) | 2022.03.05 |
BottomNavigationView를 이용한 Fragment 전환 (0) | 2021.08.02 |
TabLayout + ViewPager2를 이용한 화면 구성 (0) | 2021.08.02 |
DI (Koin)을 이용한 의존성 주입 (0) | 2021.08.02 |