이전글 ( https://velog.io/@jericho3/Multi-View-Types-RecyclerView ) 에 이어 소소하게 개선해보았다.
뷰홀더를 실드클래스로 묶는 건 해봤더니 좋지 않았다.
기존에서 바뀐 점은 CommonViewHolder를 도입해 onBind를 통합시켰고, 일부 로직 최적화, 리사이클러뷰어댑터를 리스트어댑터로 전환 정도이다.
//
sealed interface MultiView {
enum class Type {
HEADER,
POPULAR,
CATEGORY,
VIDEO,
LOADING,
}
val viewType: Type
data object Header : MultiView {
override val viewType: Type = Type.HEADER
}
data class Popular(
val videoAdapter: PopularVideoAdapter
) : MultiView {
override val viewType: Type = Type.POPULAR
}
data class Category(
val categoryAdapter: CategoryAdapter,
) : MultiView {
override val viewType: Type = Type.CATEGORY
}
data class Video(
val videoItemData: VideoItemData,
) : MultiView {
override val viewType: Type = Type.VIDEO
}
data object Loading : MultiView {
override val viewType: Type = Type.LOADING
}
}
뷰타입과 중첩 어댑터, 필요한 데이터 등을 실드 인터페이스로 묶어 관리해준다.
//
abstract class CommonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun onBind(item: MultiView)
}
abstract로, 기존 뷰홀더에 onBind만 넣어주는 단순한 구조다.
(interface로 못만드는 이유는 파라미터를 받아야 하기 때문에.)
//
class HomeAdapter(
private val onCategorySettingClick: () -> Unit,
private val onVideoClick: (item: VideoItemData) -> Int?
) : ListAdapter<MultiView, CommonViewHolder>(object : DiffUtil.ItemCallback<MultiView>() {
override fun areItemsTheSame(oldItem: MultiView, newItem: MultiView): Boolean =
oldItem === newItem
override fun areContentsTheSame(oldItem: MultiView, newItem: MultiView): Boolean =
oldItem == newItem
}) {
파라미터는 필요한 람다식을 받으면 되고, 어댑터의 뷰홀더 타입으로는 공통되는 타입을 설정한다. (<MultiView, CommonViewHolder>)
// 여기에서는 예시로 하나만 넣었다. 나머지는 전체 코드에.
inner class CategoryHolder(private val b: HomeItemCategoryBinding) :
CommonViewHolder(b.root) {
private lateinit var adapter: CategoryAdapter
init {
b.btnFlCategorySetting.setOnClickListener { onCategorySettingClick() }
tvCategoryEmptyText = b.tvCategoryEmptyText
}
override fun onBind(item: MultiView) {
if (this::adapter.isInitialized.not()) {
adapter = (item as MultiView.Category).categoryAdapter
b.rvCategoryCategories.adapter = item.categoryAdapter
}
b.tvCategoryEmptyText.isVisible = adapter.itemCount == 0
}
}
뷰홀더 클래스들을 작성해준다.
inner class여야 어댑터에서 받은 람다식들을 사용할 수 있다.
(이거 때문에 외부 실드 인터페이스에 빼려고 하다가 되돌림)
//
override fun getItemViewType(position: Int): Int = getItem(position).viewType.ordinal
getItemViewType은 sealed interface와 enum ordinal을 사용해서 이렇게 간단하게 된다.
//
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder {
return when (MultiView.Type.values()[viewType]) {
MultiView.Type.HEADER -> HeaderHolder(
ItemHeaderBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.POPULAR -> PopularHolder(
HomeItemPopularBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.CATEGORY -> CategoryHolder(
HomeItemCategoryBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.VIDEO -> VideoHolder(
VideoItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.LOADING -> LoadingHolder(
ItemLoadingProgressBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
}
override fun onBindViewHolder(holder: CommonViewHolder, position: Int) {
holder.onBind(getItem(position))
}
onCreate의 경우는 Int로 들어오는 뷰타입을 이넘.values()[viewType] 으로 exhaustive 하게 만들 수 있다.
(팩토리 등으로 통합시켜도 결국 뷰타입 확인해서 각 뷰홀더를 각 뷰바인딩으로 생성해줘야 하기에 어쩔 수 없는 것 같다. parent를 넘겨서 뷰홀더에서 직접 binding을 생성하게끔도 시도해봤지만 뷰홀더 클래스 상속 시 root view를 넘겨줘야 하기 때문에 결국 불가능했다.)
onBind는 CommonViewHolder를 도입하면서 통합시켰다.
(아이템 파라미터를 아예 없애버리는 것도..?)
리스트어댑터로 바꾸면서 onBind에서 아이템을 넘겨받아야만 MultiView 클래스의 프로퍼티에 접근할 수 있었던 한계점을 극복했다.
(그래서 뭔가를 개선했었는데... 기억이 안난다...)
//
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import coil.load
import coil.request.CachePolicy
import com.example.pintube.databinding.HomeItemCategoryBinding
import com.example.pintube.databinding.HomeItemPopularBinding
import com.example.pintube.databinding.ItemHeaderBinding
import com.example.pintube.databinding.ItemLoadingProgressBinding
import com.example.pintube.databinding.VideoItemBinding
sealed interface MultiView {
enum class Type {
HEADER,
POPULAR,
CATEGORY,
VIDEO,
LOADING,
}
val viewType: Type
data object Header : MultiView {
override val viewType: Type = Type.HEADER
}
data class Popular(
val videoAdapter: PopularVideoAdapter
) : MultiView {
override val viewType: Type = Type.POPULAR
}
data class Category(
val categoryAdapter: CategoryAdapter,
) : MultiView {
override val viewType: Type = Type.CATEGORY
}
data class Video(
val videoItemData: VideoItemData,
) : MultiView {
override val viewType: Type = Type.VIDEO
}
data object Loading : MultiView {
override val viewType: Type = Type.LOADING
}
}
abstract class CommonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun onBind(item: MultiView)
}
class HomeAdapter(
private val onCategorySettingClick: () -> Unit,
private val onVideoClick: (item: VideoItemData) -> Int?
) : ListAdapter<MultiView, CommonViewHolder>(object : DiffUtil.ItemCallback<MultiView>() {
override fun areItemsTheSame(oldItem: MultiView, newItem: MultiView): Boolean =
oldItem === newItem
override fun areContentsTheSame(oldItem: MultiView, newItem: MultiView): Boolean =
oldItem == newItem
}) {
var tvCategoryEmptyText: TextView? = null
inner class HeaderHolder(b: ItemHeaderBinding) : CommonViewHolder(b.root) {
override fun onBind(item: MultiView) {}
}
inner class PopularHolder(private val b: HomeItemPopularBinding) :
CommonViewHolder(b.root) {
init {
b.rvPopularVideos.orientation = ViewPager2.ORIENTATION_HORIZONTAL
}
override fun onBind(item: MultiView) {
// Log.d("jj-홈어댑터 popular onBind", item.toString()) //ddd
if (b.rvPopularVideos.adapter == null)
b.rvPopularVideos.adapter = (item as MultiView.Popular).videoAdapter
}
}
inner class CategoryHolder(private val b: HomeItemCategoryBinding) :
CommonViewHolder(b.root) {
private lateinit var adapter: CategoryAdapter
init {
b.btnFlCategorySetting.setOnClickListener { onCategorySettingClick() }
tvCategoryEmptyText = b.tvCategoryEmptyText
}
override fun onBind(item: MultiView) {
if (this::adapter.isInitialized.not()) {
adapter = (item as MultiView.Category).categoryAdapter
b.rvCategoryCategories.adapter = item.categoryAdapter
}
b.tvCategoryEmptyText.isVisible = adapter.itemCount == 0
}
}
inner class VideoHolder(private val b: VideoItemBinding) :
CommonViewHolder(b.root) {
init {
b.root.setOnClickListener {
Log.d(
"jj-홈 아이템(비디오) 클릭",
"$adapterPosition: ${(getItem(adapterPosition) as MultiView.Video)}"
)
onVideoClick((getItem(adapterPosition) as MultiView.Video).videoItemData)
}
}
override fun onBind(item: MultiView) {
(item as MultiView.Video).videoItemData.videoThumbnailUri?.let {
b.ivItemVideo.load(it) {
crossfade(true)
allowHardware(true)
if (adapterPosition < 5) diskCachePolicy(CachePolicy.ENABLED)
else memoryCachePolicy(CachePolicy.ENABLED)
}
}
item.videoItemData.channelThumbnailUri?.let {
b.ivItemChannel.load(it) {
crossfade(true)
allowHardware(true)
}
}
item.videoItemData.title?.let { b.tvItemTitle.text = it }
item.videoItemData.channelName?.let { b.tvItemName.text = it }
item.videoItemData.views?.let { b.tvItemViews.text = it }
item.videoItemData.date?.let { b.tvItemDate.text = it }
item.videoItemData.length?.let { b.tvPopularItemLength.text = it }
}
}
inner class LoadingHolder(private val b: ItemLoadingProgressBinding) :
CommonViewHolder(b.root) {
override fun onBind(item: MultiView) {
// Log.d("jj-LoadingHolder onBind", "${b.root}")
b.root.isVisible = itemCount > 4
}
}
override fun getItemViewType(position: Int): Int = getItem(position).viewType.ordinal
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommonViewHolder {
return when (MultiView.Type.values()[viewType]) {
MultiView.Type.HEADER -> HeaderHolder(
ItemHeaderBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.POPULAR -> PopularHolder(
HomeItemPopularBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.CATEGORY -> CategoryHolder(
HomeItemCategoryBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.VIDEO -> VideoHolder(
VideoItemBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
MultiView.Type.LOADING -> LoadingHolder(
ItemLoadingProgressBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
}
override fun onBindViewHolder(holder: CommonViewHolder, position: Int) {
holder.onBind(getItem(position))
}
}