본문 바로가기
IT/Android

Epoxy 사용법 정리

by YEON-DU 2021. 1. 1.
반응형

에어비앤비 라이브러리 Epoxy

requestModelBuild() = notifyDataSetChanged() 와 동일하다.

buildModels()의 함수를 재 실행한다.

buildModels에는 ItemHolder나 Item(model)을 구현하여서 넣을 수 있다.

케이스에 따라 addIf를 추가하여 특정 Boolean일 때만 해당 모델이 빌드되도록 만들 수도 있다.

이 기능을 이용하여 로딩중일 때나, 오류가 났을 때 보여질 모델을 따로 추가할 수 있다.

// build.gradle
implementation "com.airbnb.android:epoxy:4.2.0"
implementation 'com.airbnb.android:epoxy-paging:4.1.0'

// Add the annotation processor if you are using Epoxy's annotations (recommended)
apt "com.airbnb.android:epoxy-processor:4.2.0"

다음과 같이 gradle에 추가하여 사용할 수 있다.

epoxy-paging의 경우에는 paging 라이브러리와 함께 epoxy를 사용할 때 추가해주면 된다.

 

EpoxyHolder

@EpoxyModelClass(layout = R.layout.epoxy_model)
abstract class TestModel : EpoxyModelWithHolder<EpoxyModel.Holder>() {

    @EpoxyAttribute
    var itemText: String? = null

        @EpoxyAttribute
    lateinit var listener: () -> Unit

    override fun bind(holder: Holder) {
        holder.textView.text = itemText
                holder.button.setOnclickListener { listener() }
    }

    class Holder : EpoxyHolder() {
        lateinit var tv_text: TextView
        lateinit var btn: Button
        override fun bindView(itemView: View) {
            textView = itemView.findViewById(R.id.tv_text)
            button = itemView.findViewById(R.id.btn)
        }
    }
}

item을 정의하는 방식은 다른 Holder와 비슷하게 정의한다.

 

@EpoxyModelClass(layout = R.layout.epoxy_model)
abstract class TestModel : ViewBindingEpoxyModelWithHolder<EpoxyModelBinding>() {

    @EpoxyAttribute
    var itemText: String? = null

    @EpoxyAttribute
    lateinit var listener: () -> Unit

    override fun ItemImageBinding.bind() {
        tv_text.text = itemText
        btn.setOnClickListener { listener() }
    }

    override fun ItemImageBinding.unbind() {

    }

뷰 바인딩을 사용하여 이처럼 정의할 수도 있다.

 

EpoxyController

EpoxyController는 adapter와 같은 기능으로, 실제로 뷰와 연결하여 사용할 때는

view.adapter = epoxyController.adapter 와 같이 연결하여 사용하면 된다.

buildModels() 함수를 override하여 사용한다.

 

import com.airbnb.epoxy.EpoxyController

class TestController : EpoxyController() {
    private var data: List<데이터 타입> = Collections.emptyList()
        private var isLoading = true
        private var isError = true

    fun setData(data: List<데이터 타입>?) {
        data?.let {
            this.data = data
            requestModelBuild()
        }
    }

    override fun buildModels() {
        data?.forEachIndexed { index, s ->
                        if (index == 2) {
                stickyItemEpoxyHolder {
                    id("sticky-header $0")
                    title("Sticky header $0")
                    spanSizeOverride { totalSpanCount, position, itemCount -> 1 }
                }
            }
            TestModel {
                id(index)
                data(s)
                spanSizeOverride { _, _, _ -> 1 } // 아이템 한 개가 몇 칸을 차지하는 지
                listener {
                    // 리스너 동작 정의
                }
            }
        }

        LoadingEpoxyModel_().apply {
                        id("loading")
        }
            .addIf(isLoading, this)

        ErrorEpoxyModel_().apply {
                        id("error")
        }
            .addIf(isError, this)

    }

}

 

setData와 같은 함수를 정의하여 외부에서 호출하여 데이터가 반영되도록 할 수 있다.

이외에도 isLoading이나 isError를 set하는 함수를 정의하여 이후 requestModelBuild가 되도록 만들고, 새로운 item이 나오도록 만들 수도 있다.

data?.let을 이용하여 model을 정의할 수도 있지만 data?.forEachIndexed 내부에서 .rem() (나머지 연산. % 연산과 동일)를 사용하여 특정 인덱스에 반복적으로 다른 아이템이 나타나도록 만들 수도 있다.

EpoxyModel 객체들은 EpoxyModel_와 같이 _가 붙은 형태로 gradle 빌드 이후로 epoxy가 자체적으로 만들어주는 객체로 나타난다. 초기에 빌드 이전에는 컴파일 오류로 표시되지만 빌드 이후 파일이 생성된다.

epoxy는 gridLayout과 Sticky Header를 지원한다. gridLayout을 쓸 때에는 spanSizeOverride에 사용한 칸 수가 gridLayout에서 쓴 spanSize와 비교해서 불가능한 경우 런타임 에러가 나타난다.

Controller 분석

controller의 adapter의 get 함수들은 다음과 같다.

함수 이름 자체도 getModelAtPosition라는 함수를 사용해서, 실제 Position에 따라 모델명을 가져온다.

(어떠한 ItemHolder 썼는지, id, viewType(동일한 뷰타입 - 홀더인 경우 동일한 값), shown, addedToAdapter)

 

Adapter get Functions

get 함수 내의 position 파라미터는 전체 아이템을 모델 구분 없이 지정한 것이다.

getItemId(position: Int) position에 있는 Item의 Id를 가져온다.

getModelAtPosition(position: Int) position에 있는 모델명을 가져온다.

getModelById(id: Int) ID별 모델명을 가져온다.

getItemViewType(position: Int) position에 있는 Item의 ViewType을 가져온다.

itemCount는 모든 모델의 전체 item 개수를 반환한다.

 

Sticky Header 판별 기준

isStickyHeader 함수를 이용하여 전체 itemCount만큼 Model들을 보면서 sticky header인 경우 headerPositions 리스트에 저장한다.

현재는 forEachIndexed를 사용하여 데이터 개수만큼 임의로 index를 만들어주고 있다. sticky header에 지정된 임의의 index를 사용하여 위치를 저장하게 되면 해당 정보는 index로 지속적으로 viewModel 내에서 저장하고 있으므로 sticky되고 난 이후에도 동일한 index를 사용하게 된다.

 

onVisibilityChanged, onVisibilityStateChanged 함수

EpoxyVisibilityTracker().attach(visible 상태를 체크할 RecyclerView)

Fragment나 Activity 내에서 선언하여 해당 함수를 오버라이딩하면 Item별 visibility를 체크할 수 있다.

 

VisibilityState

VISIBLE (0) Item이 VISBLE일 때 자동으로 호출됨
INVISIBLE (1) Item이 INVISIBLE일 때 자동으로 호출됨
FOCUSED_VISIBLE (2) 해당 아이템이 Focus(EX. Touch Event) 할 수 있을 때 호출됨
UNFOCUSED_VISIBLE (3) 해당 아이템이 Focus(EX. Touch Event) 할 수 없을 때 호출됨
FULL_IMPRESSION_VISIBLE (4) 현재 스크롤이 해당 아이템에 있을 때 호출됨

아래의 변수들은 EpoxyVisibilityTracker에서 setPartialImpressionThresholdPercentage 함수에 임계값을 설정하여 그 이상으로 RecyclerView에 아이템이 나타나게 되면 VISBLE을, 아니면 INVISIBLE을 반환한다.
PARTIAL_IMPRESSION_VISIBLE (5)
PARTIAL_IMPRESSION_INVISIBLE (6)

 

Sticky Header가 Sticky 될 때의 VisibilityState 변화 과정

StickyHeader (Sticky되는 Header) VISIBLE → FOCUSED_VISIBLE → FULL_IMPRESSION_VISIBLE

StickyHeader (기존 RecyclerView에 남아있는 Header) VISIBLE, FOCUSED_VISIBLE, FULL_IMPRESSION_VISIBLE → UNFOCUSED_VISIBLE → INVISIBLE

 

Sticky Header 가 Sticky 되었다가 해제될 때의 VisibilityState 변화 과정

StickyHeader (Sticky 되어있던 Header) INVISIBLE → UNFOCUSED_VISIBLE

StickyHeader (기존 RecyclerView에 남아있던 Header) VISIBLE → FOCUSED_VISIBLE → FULL_IMPRESSION_VISIBLE

 

 

airbnb/epoxy

 

airbnb/epoxy

Epoxy is an Android library for building complex screens in a RecyclerView - airbnb/epoxy

github.com

 

반응형

'IT > Android' 카테고리의 다른 글

Fragment에서 Back키 처리 방식  (0) 2021.04.04
21.04.03  (0) 2021.04.03
람다와 익명클래스 괴담  (0) 2021.03.14
Serializable vs Parcelable  (0) 2021.02.19
[Android Studio] OpenCV 적용  (0) 2019.10.16

댓글