Android

리사이클러뷰 중첩 구조를 하나의 리사이클러뷰로 리팩토링 과정

그란. 2021. 9. 29. 10:00

아래와 같은 UI를 구현할때 어떻게 하는게 좋을까?

보통 기본적으로는 ScrollView 안에 TextView(헤더 내용), RecyclerView(이미지), TextView(하단 내용)으로 구성한다. 

 

 

이렇게 하면 ScrollView의 세로 스크롤과 RecyclerView의 세로 스크롤이 겹치기 때문에 스크롤 이슈가 있을 수 있다.

또한 스크롤 뷰에

android:fillViewport="true" 속성을 넣게 되면

스크롤뷰가 content높이 계산을 하기 위해 RecyclerView가 모든 아이템을 그리게되는데 viewholder재사용을 하지 않아 RecyclerView의 이점을 얻을 수 없는 문제도 있다.

 

그래서 하나의 리사이클러뷰로 구조를 변경하는것이 좋다 ( ViewType 쪼개기 )

 

 

상단 TextView (헤더), ImageView(이미지), TextView(푸터) 로 3가지 타입으로 나눈다.

 

해당 형태의 모델로 변경

sealed class NoticeDetailContentUiModel : Identifiable {

    data class Header(
        val content: String
    ) : NoticeDetailContentUiModel() {
        override val identifier: Any
            get() = "Header"
    }

    data class Image(
        val id: Long,
        val src: String
    ) : NoticeDetailContentUiModel() {
        override val identifier: Any
            get() = id
    }

    data class Footer(
        val description: String?
    ) : NoticeDetailContentUiModel() {
        override val identifier: Any
            get() = "Footer"
    }
}

 

Adapter 에서 다음과 같이 설정

 override fun getItemViewType(position: Int, item: NoticeDetailContentUiModel): ViewType {
        return when (item) {
            is NoticeDetailContentUiModel.Header -> ViewType.HEADER
            is NoticeDetailContentUiModel.Image -> ViewType.IMAGE
            is NoticeDetailContentUiModel.Footer -> ViewType.FOOTER
        }
    }
    
     override fun onCreateViewHolder(
        layoutInflater: LayoutInflater,
        parent: ViewGroup,
        viewType: ViewType
    ): BaseViewHolder<NoticeDetailContentUiModel> {
        return when (viewType) {
            ViewType.HEADER -> ItemViewHolder(
                layoutInflater.inflate(
                    R.layout.item_notice_detail_header,
                    parent,
                    false
                )
            )
            ViewType.IMAGE -> ItemViewHolder(
                layoutInflater.inflate(
                    R.layout.item_notice_detail_image,
                    parent,
                    false
                )
            )
            ViewType.FOOTER -> ItemViewHolder(
                layoutInflater.inflate(
                    R.layout.item_notice_detail_footer,
                    parent,
                    false
                )
            )
        }
    }
    
enum class ViewType {
        HEADER, IMAGE, FOOTER;
}

응용 버전 

이미지와 같이 ViewPager + RecyclerView (GridLayoutManager)의 경우에는 어떻게 해야할까?

 

1. 모델 

sealed class HomeUiModel : Identifiable {

    data class BannerList(
        val banners: List<Banner>
    ) : HomeUiModel() {
        override val identifier: Any
            get() = "banners"

        data class Banner(
            val id: Long,
            val image: String
        ) : Identifiable {
            override val identifier: Any
                get() = id
        }
    }

    data class Product(
        val id: Long,
        val image: String
    ) : HomeUiModel() {
        override val identifier: Any
            get() = id
    }
}

 

2. UI (xml)

 

- BannerList 

 <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/vp_banner"
            android:layout_width="0dp"
            android:layout_height="98dp" />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tl_banner"
            android:layout_width="wrap_content"
            android:layout_height="30dp" />

    </androidx.constraintlayout.widget.ConstraintLayout>

- Banner

<ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

- Product

   <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="@{()->item.onClick.invoke()}">

        <ImageView
            android:id="@+id/iv_product"
            android:layout_width="0dp"
            android:layout_height="0dp"/>

		...
        
</androidx.constraintlayout.widget.ConstraintLayout>

 

 - Adapter

 override fun getItemViewType(position: Int, item: HomeUiModel): ViewType {
        return when (item) {
            is HomeUiModel.BannerList -> ViewType.BANNER_LIST
            is HomeUiModel.Product -> ViewType.PRODUCT
        }
    }

    override fun onCreateViewHolder(
        layoutInflater: LayoutInflater,
        parent: ViewGroup,
        viewType: ViewType
    ): BaseViewHolder<HomeUiModel> {
        return when (viewType) {
            ViewType.BANNER_LIST -> BannerViewHolder(
                ItemHomeBannerListBinding.inflate(
                    layoutInflater,
                    parent,
                    false
                )
            )

            ViewType.PRODUCT -> ProductListViewHolder(
                ItemHomeProductBinding.inflate(
                    layoutInflater,
                    parent,
                    false
                )
            )
        }
    }

    class BannerViewHolder(val binding: ItemHomeBannerListBinding) :
        BaseViewHolder<HomeUiModel>(binding.root) {
        init {
            with(binding) {
                vpBanner.adapter = HomeBannerListAdapter()
                TabLayoutMediator(tlBanner, vpBanner) { tab, _ ->
                    vpBanner.currentItem = tab.position
                }.attach()
            }
        }
    }

    enum class ViewType {
        BANNER_LIST, PRODUCT
    }

 

* 전체 리사이클러뷰의 구조는 GridLayoutManger, spanCount="2"로 지정한다. 

app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"

 

-> 뷰타입이 BANNER_LIST 인 경우엔 spanSize : 2 (늘리기) 

PRODUCT 인 경우엔 1 (그대로) 

(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    return when (outerAdapter.getItemViewTypeAsEnum(position)) {
                        HomeOuterAdapter.ViewType.BANNER_LIST -> 2
                        HomeOuterAdapter.ViewType.PRODUCT -> 1
                    }
                }
            }