Android/Jetpack Compose

[Android] Epoxy를 사용하여 RecyclerView를 쉽게 사용해보자 -1. 기본

Heeg's 2022. 9. 1. 22:28
728x90

기술 블로그를 확인하다, Epoxy라는 유용해 보이는 라이브러리를 발견하였다.

RecyclerView를 보다 사용하기 쉽게 도와주는 라이브러리라고 해서 찾아보고, 간단하게 적용해보았다.

 

해당 라이브러리를 사용하고, 가이드 한 글은 외국 블로그에서 많이 찾아볼 수 있었는데,

생각보다 따라서 구현했을 때 정상적으로 한 번에 실행되는 경우가 없었던 것 같다.

 

따라서, 해당 라이브러리의 Github를 토대로 필자가 적용한 순서대로 정리하여 글을 작성해 보았다.


우선,

Epoxy가 무엇인가 ?

Airbnb 사에서 만든 라이브러리로,
RecyclerView에서 복잡한 화면을 쉽게 구현하는 것을 도와주는 라이브러리.

이다.

 

RecyclerView를 사용하고 화면에 보여주기 위해서는 Adapter와 ViewHolder를 만들고, RecyclerView의 adapter에 만든 Adapter 객체를 넣고.. 등등 많은 작업을 수행해주어야 하는데,

Epoxy를 사용하게 되면 DataModel, Controller만 선언하면 RecyclerView를 사용할 수 있다. 사용해 보니, 보일러 플레이트 코드를 줄여 비교적 적은 코드로 위의 작업을 수행할 수 있게 도와주는 것으로 보인다.

 

물론, DataBinding을 사용하여 Epoxy를 구현하게 되면 훨씬 더 간단하고 적은 코드로 구현할 수 있지만 이번 글에서는 기본적인 사용 방법으로 구현해보도록 하겠다.

해당 사용 방법은 Airbnb의 Github에서 참고하여 작성하였다.

 

Epoxy를 사용하기 위해서,

Module 범위의 gradle에 값들을 추가해주도록 하자.

 

plugins {
    ...
    id 'kotlin-kapt'
}

android {
    ...
}

kapt {
    correctErrorTypes = true
}

dependencies {
    ...
    def epoxy_version = "5.0.0-beta05"
    implementation "com.airbnb.android:epoxy:$epoxy_version"
    kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
}

 

여기서 kotlin을 사용하지 않는다면 dependency만 추가해주면 된다.

사용하는 epoxy 버전은 github에 나와있는 가장 최신 버전을 사용하도록 하였다.

 

Epoxy를 사용하기 위하여 디펜던시를 추가해주었으니, epoxy를 적용해보도록 하겠다.

적용하기 위한 작업 순서는 다음과 같다.

 

  1. Layout 설정
  2. DataModel Class 생성
  3. Controller Class 생성
  4. EpoxyRecyclerView 사용

 

각 순서에서 작업할 내용은 많지 않으니 순서대로 진행해보도록 하겠다.

 

우선,

xml에 EpoxyRecyclerView를 선언해주도록 하자.

 

<com.airbnb.epoxy.EpoxyRecyclerView
    android:id="@+id/epoxyRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:listitem="@layout/epoxy_data_view" />

 

 

별 다른 점 없이 recyclerView를 선언하는 것처럼 EpoxyRecyclerView를 선언해주면 된다.

여기서 확인해야 할 부분은 tools:listitem 부분인데, 해당 부분은 없어도 무관한 부분이며, recyclerview에 사용할 내부 layout을 위와 같이 설정해주면 Preview로 상태를 볼 수 있게 된다.

Preview로 볼 수 있는 것 외에는 실제 화면에서는 달라지는 사항은 없으니 생략해도 된다.

 

<TextView
    android:id="@+id/epoxyTitle"
    android:layout_width="120dp"
    android:layout_height="40dp"
    android:text="@{title}"
    android:gravity="center"/>

 

 

epoxy_data_view.xml은 다음과 같이 TextView 하나만 설정해주도록 하자.

필자가 작성한 예제에는 최상단 <layout> 태그를 사용하고, <data> 태그를 통해 Databinding 관련된 부분이 작성되어 있지만, ConstraintLayout을 사용하여 선언해도 무관하다.

 

이렇게 xml 파일을 만들어 주었으면, layout 설정은 끝난 것이다.

EpoxyRecyclerView를 사용하여 RecyclerView를 표현할 것이고, 내부에서는 epoxy_data_view.xml에 선언한 것처럼 하나의 textView를 가지고 있는 아이템이 반복되게 될 것이다.

 

다음으로,

DataModel을 만들어보자.

 

@EpoxyModelClass(layout = R.layout.epoxy_data_view)
abstract class EpoxyDataModel : EpoxyModelWithHolder<EpoxyDataModel.TitleHolder>() {

    @EpoxyAttribute
    var title: String? = ""

    override fun bind(holder: TitleHolder) {
        holder.textView.text = title
    }

    inner class TitleHolder : EpoxyHolder() {
        lateinit var textView: TextView
        override fun bindView(itemView: View) {
            textView = itemView.findViewById(R.id.epoxyTitle)
        }
    }
}

 

아주 짧게 DataModel을 만들 수 있다.

 

가장 상단에 위치한 @EpoxyModelClass 어노테이션을 사용하여, 해당 데이터 모델이 바라보고 있는 레이아웃이 무엇인지 명시해주도록 한다.

필자는 위에 선언한 epoxy_data_view를 사용할 것이기 때문에 위와 같이 설정해 주었다.

 

EpoxyModelWithHolder를 상속받아서 사용하며, inner class로 선언할 Holder를 EpoxyDataModel.~ 에 넣어주도록 한다.

 

다음으로 보이는 @EpoxyAttribute 어노테이션필요한 변수를 바인딩하기 위해 선언한다.

해당 값은 Controller에서 사용하는 부분으로 RecyclerView에 데이터를 추가할 때, 해당 어노테이션으로 선언한 변수명을 사용하여 값을 전달하게 된다.

필자가 만든 예제에서는 하나의 String 값만 사용하기 때문에 var title 하나만 선언하여 사용하였지만, 여러 개의 값을 사용해야 하는 경우 각 필요한 변수에 @EpoxyAttribute를 중복하여 선언하여 사용하면 된다.

 

inner class의 Holder 클래스는 EpoxyHolder를 상속받아서 구현하며,

Holder 부분은 기존 RecyclerView에서 사용하는 것과 동일한 역할로, 레이아웃에 원하는 데이터를 바인딩 하기 위하여 명시적으로 설정해주는 역할을 한다.

따라서, override 된 bindView 함수에서 epoxy_data_view에서 선언한 item을 DataModel에서 사용하기 위해 lateinit을 통해 늦은 초기화를 시켜주도록 한다.

 

그 후, DataModel에서 override 할 수 있는 bind 함수를 통해 원하는 layout item과 원하는 데이터를 바인딩시켜주도록 한다.

 

아주 간단한 구조이기 때문에, 본 예제와 다른 구조인 layout을 사용하더라도 쉽게 적용할 수 있을 것이다.

 

세 번째로,

Controller 부분을 구현해 보자.

 

class EpoxyController : EpoxyController() {
    private val insertData = ArrayList<Title>()

    init {
        insertData.add(Title("1"))
            ...
    }

    override fun buildModels() {
        insertData.forEach {
            EpoxyDataModel_()
                .id("insertData")
                .title("it : $it")
                .addTo(this)
        }
    }
}

 

이번에도 아주 간단하게 구현이 가능하다.

 

EpoxyController를 상속받아서 사용하면 되고, insertData와 init에 선언한 부분은 임의로 ArrayList 데이터를 만들어서 사용하기 위한 방법을 보여주기 위해 선언한 것이다.

정말 간단하게 정상 동작하는 것만 확인하기 위해서는

 

for (index in 0 .. 10) {
    EpoxyDataModel_()
        .id(index)
        .title("it : $index")
        .addTo(this)
}

 

 

buildModels()에 이렇게만 선언하고 사용해도 무관하다.

 

 

여기서 조심해야 할 부분은, id 값에 대한 설정이 반드시 필요하단 것이다.

id 값을 설정하지 않으면 다음과 같은 오류가 발생한다.

 

com.airbnb.epoxy.IllegalEpoxyUsage: You must set an id on a model before adding it. Use the @AutoModel annotation if you want an id to be automatically generated for you.

 

직접 id 값을 추가하거나 @AutoModel 어노테이션을 사용하여 자동으로 id 값을 추가하도록 해야 한다고 한다.

필자는 @AutoModel 어노테이션을 사용하여 추가하려고 많은 시행착오를 겪었으나, 해당 어노테이션을 사용하는 방법을 찾지 못하고 id 값을 수동으로 추가하는 방법을 사용하였다.

 

다음으로 title 값은 DataModel에서 @EpoxyAttribute 어노테이션을 추가하여 선언했던 변수를 사용하면 된다.

위에서 언급했듯이 해당 변수명을 사용하여 Controller에서 데이터를 설정해주는 것이다.

 

해당 모델에서 필요한 값을 모두 설정해 주었으면, 마지막에 addTo를 통해 현재 컨트롤러에 설정한 데이터를 추가해주면 Controller에서 진행할 작업은 모두 끝나게 된다.

 

마지막으로,

EpoxyRecyclerView를 사용해주면 된다.

 

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val controller: EpoxyController by lazy { EpoxyController() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
            ...
        setItem()
    }
    private fun setItem() {
        val linearlayoutManager = LinearLayoutManager(this)

        binding.epoxyRecyclerView.apply {
            layoutManager = linearlayoutManager
            setHasFixedSize(true)
            adapter = controller.adapter
        }

        controller.requestModelBuild()
    }
}

 

우선, 해당 부분은 다시 변경하기가 귀찮아서 그대로 dataBinding을 사용하여 layout의 아이템을 가져오도록 하였다는 점을 참고하길 바란다.

 

private val controller: EpoxyController by lazy { EpoxyController() }

 

by lazy를 사용하여 controller에 대한 늦은 초기화를 수행해주도록 한다. 초기화 한 객체인 EpoxyController는 위에 선언한 Controller Class이다.

여기서 controller 객체는 RecyclerView에서 사용한 것처럼, epoxy RecyclerView의 adapter에 해당 위에서 선언한 controller를 추가해주기 위해서 선언한다.

 

setItem 함수에서는 epoxyRecyclerView에 대한 설정을 추가해주면 된다.

여기서 설정하는 부분은 기존 사용하던 recyclerView와 동일하게 사용할 수 있다.

 

private fun useRecyclerView() {
    val layoutManager = LinearLayoutManager(this)
    binding!!.recyclerView.layoutManager = layoutManager
    itemAdpater = Adapter(this, viewModel, childFragmentManager)
    binding!!.recyclerView.adapter = itemAdpater
}

 

private fun useEpoxyRecyclerView() {
    val linearlayoutManager = LinearLayoutManager(this)
    binding.epoxyRecyclerView.layoutManager = linearlayoutManager
    binding.epoxyRecyclerView.adapter = controller.adapter

    controller.requestModelBuild()
}

 

apply로 사용된 부분을 풀어서 사용하고, 필수 설정만 확인하면 이처럼 거의 동일하게 사용할 수 있다.

 

다른 부분이라고 하면, controller에서 추가한 item을 epoxyRecyclerView에 update 하기 위해서는 requestModelBuild를 호출해주어야 한다는 것이다.

기존 recyclerView에서는 notify를 통해 화면을 갱신했던 것과는 차이가 존재한다.

 

이렇게 코드를 구현한 후, 빌드를 해보면 다음과 같은 결과를 볼 수 있다.

 

 

Default로 설정된 값이 Vertical이기 때문에 가로로 데이터를 보여주고 싶다면

 

linearlayoutManager.orientation = LinearLayoutManager.HORIZONTAL

 

를 통해 Horizontal 설정으로 변경해주면 되고,

 

addItemDecoration(
    DividerItemDecoration(
        this@MainActivity,
        linearlayoutManager.orientation
    )
)

 

epoxyRecyclerView에 다음과 같은 설정을 추가하여 간단하게 Divider를 추가할 수도 있다.


Controller에서 데이터를 추가하는 방식이 아닌, Activity에서 epoxyRecyclerView에 추가할 데이터를 전달하는 방법도 존재한다.

 

class EpoxyController : TypedEpoxyController<ArrayList<Title>>() {
    override fun buildModels(data: ArrayList<Title>?) {
        data?.forEach {
            EpoxyDataModel_()
                .id("DATA")
                .title("it : $it")
                .addTo(this)
        }
    }
}

 

 

EpoxyController가 아닌 TypedEpoxyController를 사용하며, 전달받을 Data Type을 <> 안에 선언해주면 된다.

 

override 되는 함수인 buildModels()도 전달받는 데이터가 생겼으므로 해당 타입을 인자로 받아서 사용할 수 있게 변경된다.

여기서, 전달할 데이터 타입이 늘어나게 되면 Typed2, Typed3EpoxyController처럼 숫자를 늘려가며 사용하면 되고,

 

Typed4EpoxyController<String, String, String, String>()

 

사용하는 방법 또한 동일하게 매개 변수만 늘려가면서 사용할 수 있다.

 

이렇게 Controller를 변경한 후에, Activity에서 데이터를 Controller에게 던져주도록 변경을 해야 한다.

기존에 requestModelBuild()를 사용하게 된다면,

 

You cannot call `requestModelBuild` directly. Call `setData` instead to trigger a model refresh with new data.

 

다음과 같은 에러가 발생하게 된다.

에러 내용에서 확인한 것처럼 setData를 호출해주어야 하므로,

 

//        controller.requestModelBuild()
        controller.setData(dummyData)

 

이처럼 setData와 필요한 데이터를 매개변수로 넣어서 전달해주는 형식으로 호출하면 된다.


기본적인 방법으로 Epoxy를 사용해 보았다.

RecyclerView 대신 Epoxy를 사용하는 것이 확실히 좋다.라고는 말할 수는 없지만, 전반적으로 코드의 양이 줄어드는 것은 확실하기 때문에 보다 효율적으로, 보다 편리하게 RecyclerView를 사용할 수 있는 것 같다.

 

Controller와 DataModel을 만들어야 하며, 해당 클래스 내부의 작업들이 기존의 RecyclerView와 큰 차이가 없기 때문에 이러한 아쉬움이 남는데, 이 정도의 차이라면 지금까지 사용해 왔던 RecyclerView를 사용하는 것이 시간이 절약되지 않을까?라는 생각이 든다.

 

하지만, 

DataBinding을 사용하여 Epoxy를 사용하게 되는 경우 이러한 아쉬움을 모두 없애주는 것 같다. 오히려, DataBinding을 사용한 Epoxy가 진정한 Epoxy라고 생각이 들 정도로 크게 차이가 난다.

어노테이션을 하나 추가한 object 만 하나 생성하게 되면 Controller와 DataModel을 선언하지 않고 동일하게 사용할 수 있으니 말이다.

 

이번에는 아주 기본적인 사용 방법에 대하여 작성해 보았으니,

다음 글에서는 Epoxy에 DataBinding을 사용한 방법을 작성해보도록 하겠다.

 

해당 게시글에 사용한 예제 코드는 Github에 올려두었다.

https://github.com/HeeGyeong/EpoxySample

 

GitHub - HeeGyeong/EpoxySample

Contribute to HeeGyeong/EpoxySample development by creating an account on GitHub.

github.com

 

728x90