기존의 다른 화면의 코드들을 본 뒤 개발을 하다가 비효율적인 면이 보이게 되어, 새롭게 개발한 화면과 기존 화면의 리팩토링을 한 내용을 조금 적어보려고한다.
여러 화면을 마주하다 보면 한 개의 리싸이클러뷰 어댑터에서 여러 뷰홀더를 관리해야 하는 상황이 온다.
위와 같이 피드에 상점(FeedStore), 멤버(FeedMember), 상품(FeedItem)을 각각 표시하는 리스트 형식의 UI가 구성되어있다고 하자
아래는 RecyclerView
의 Adapter
부분 코드이다.
class FeedAdapter : ListAdapter<Feed, ViewHolder>(DiffUtilCallback()) {
override fun getItemViewType(position: Int): Int {
val item = currentList.getOrElse(position) { return FeedType.FEED_ERROR.ordinal }
return when (item) {
is FeedItem -> FEED_ITEM.ordinal
is FeedMember -> FEED_Member.ordinal
is FeedStore -> FEED_Store.ordinal
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return viewHolderFactory.createViewHolder(parent, viewType)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = currentList.getOrElse(position) { return }
when (item) {
is FeedItem -> bindFeedItem(holder, item)
is FeedMember -> bindFeedMember(holder, item)
is FeedStore -> bindFeedStore(holder, item)
else -> return
}
}
/**
* [FeedItem]을 뷰홀더에 bind
*
* @param holder [FeedItemViewHolder]
* @param item [FeedItem]
*/
private fun bindFeedItem(holder: ViewHolder, item: FeedItem) {
val feedItemHolder = holder as? FeedItemViewHolder ?: return
feedItemHolder.bind(item.uiModel)
}
/**
* [FeedMember]를 뷰홀더에 bind
*
* @param holder [FeedMemberViewHolder]
* @param item [FeedMember]
*/
private fun bindFeedMember(holder: ViewHolder, item: FeedCurationItem) {
val feedMemberHolder = holder as? FeedMemberViewHolder ?: return
feedMemberHolder.bind(item.uiModel)
}
/**
* [FeedStore]를 뷰홀더에 bind
*
* @param holder [FeedStoreViewHolder]
* @param item [FeedStore]
*/
private fun bindFeedStore(holder: ViewHolder, item: FeedCurationMember) {
val feedStoreHolder = holder as? FeedStoreViewHolder ?: return
feedStoreHolder.bind(item.uiModel)
}
}
또한 뷰홀더 팩토리에서는 아래와 같이 뷰홀더를 viewType
에 맞게 각각 생성해 줄 것이다.
class HomeFeedViewHolderFactory {
fun createViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
FEED_ITEM.ordinal -> createFeedItemViewHolder(parent)
FEED_MEMBER.ordinal -> createFeedMemberViewHolder(parent)
FEED_STORE.ordinal -> createFeedStoreViewHolder(parent)
else -> createErrorViewHolder(parent)
}
}
여기까지는 별 문제가 없어보인다. 하지만, 뷰홀더 내부를 들여다보면 비효율적인 면이 보인다.
상품, 회원, 상점 뷰홀더 모두 회원에 대한 닉네임을 표시하고 있다.
(예시 그림에서의 김짱구, 홍길동)
class FeedItemViewHolder(
private val binding: AdapterFeedItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item:FeedItemUiModel){
binding.executeAfter {
setNickName(textNick, item.nick)
}
}
/**
* 판매자 닉네임 세팅
*
* @param view [AppCompatTextView]
* @param nick 판매자 닉네임을 나타내는 String 값
*/
fun setNickName(view: AppCompatTextView, nick: String) {
view.text = nick
}
}
class FeedMemberViewHolder(
private val binding: AdapterFeedMemberBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item:FeedMemberUiModel){
binding.executeAfter {
setNickName(textNick, item.nick)
}
}
/**
* 판매자 닉네임 세팅
*
* @param view [AppCompatTextView]
* @param nick 판매자 닉네임을 나타내는 String 값
*/
fun setNickName(view: AppCompatTextView, nick: String) {
view.text = nick
}
}
class FeedStoreViewHolder(
private val binding: AdapterFeedStoreBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item:FeedStoreUiModel){
binding.executeAfter {
setNickName(textNick, item.nick)
}
}
/**
* 판매자 닉네임 세팅
*
* @param view [AppCompatTextView]
* @param nick 판매자 닉네임을 나타내는 String 값
*/
fun setNickName(view: AppCompatTextView, nick: String) {
view.text = nick
}
}
동일한 기능을 가진 setNickName
메서드가 각 뷰홀더에서 중복되고 있다.
현재는 중복되는 메서드가 하나라서 괜찮아 보일 수 있겠지만, 뷰가 늘어나고 기능이 추가된다면 무시하지 못할만큼의 불필요한 작업들이 많아질 것이다.
여러가지 고민 끝에 ViewBinder
라는 클래스를 하나 만들어서 관리하기로 했다.
이 ViewBinder
클래스의 역할은 오로지 뷰에 데이터를 결합시키는 용도 즉, 데이터 렌더링을 돕는 메서드를 제공하는 역할만 수행되어야 한다.
class FeedViewBinder {
/**
* 판매자 닉네임 세팅
*
* @param view [AppCompatTextView]
* @param nick 판매자 닉네임을 나타내는 String 값
*/
fun setNickName(view: AppCompatTextView, nick: String) {
view.text = nick
}
}
이렇게 만든 FeedViewBinder
를 각각의 뷰홀더에 주입하여 사용하면 된다.
class FeedItemViewHolder(
private val binding: AdapterFeedItemBinding,
private val feedViewBinder: FeedViewBinder
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item:FeedItemUiModel){
binding.executeAfter {
feedViewBinder.run {
setNickName(textNick, item.nick)
}
}
}
}
class FeedMemberViewHolder(
private val binding: AdapterFeedMemberBinding,
private val feedViewBinder: FeedViewBinder
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item:FeedMemberUiModel){
binding.executeAfter {
feedViewBinder.run {
setNickName(textNick, item.nick)
}
}
}
}
class FeedStoreViewHolder(
private val binding: AdapterFeedStoreBinding,
private val feedViewBinder: FeedViewBinder
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item:FeedStoreUiModel){
binding.executeAfter {
feedViewBinder.run {
setNickName(textNick, item.nick)
}
}
}
}