Epoxy

매일 수정하는 GNOSS LV5·2021년 9월 28일
1

AndroidStudio

목록 보기
14/83
post-custom-banner

왜 사용해?

  • 복잡한 형태의 Recyclerview를 좀 더 쉽게 구현하기 위해 만들어진 라이브러리입니다.
  • Airbnb에서 만들어진 오픈소스 라이브러리입니다.
  • 여러 뷰 타입을 갖는 Multi type Recyclerview에서 효율적으로 사용 가능합니다.
  • 디자인 변경에 빠르게 적응하고 코드를 모듈화하고 관리하기 쉽게 만들기 위한 많은 인터페이스를 제공합니다

어떻게 이루어져있는가?

  • Epoxy Models - 본질적으로 Recyclerview.ViewHolder입니다. Recyclerview에서 뷰가 어떻게 보이는지 설명합니다.
  • Epoxy Controllers - Recyclerview에 추가될 Model을 제어하고 Epoxy Models 또는 ViewHolder를 Recyclerview에 추가 할 수 있는 플러그인-아웃 인터페이스를 제공하므로 다양한 화면을 빠르고 쉽게 개발 가능합니다.

사용법

build.gradle(:app)

plugins {
    id 'kotlin-kapt'
}

  //Added for Epoxy
    kapt {
        correctErrorTypes = true
    }
dependencies {
	// Android RecyclerView
    implementation "androidx.recyclerview:recyclerview:1.1.0"

    // Airbnb Epoxy
    implementation "com.airbnb.android:epoxy:2.8.0"
    kapt "com.airbnb.android:epoxy-processor:2.8.0"
}

먼저 Model을 만들겠습니다.

Food

data class Food (
        val image:Int=-1,
        val title:String="",
        val description:String=""
)

다음은 EpoxyModelWithHolder를 상속받는 뷰홀더 모델을 만듭니다.

특정 뷰를 뷰홀더에 바인딩 한 다음 데이터를 전달하기 위해 RecyclerView.Adapter에서 확장 된 어댑터가 필요합니다.

SingleItemModel

@EpoxyModelClass(layout = R.layout.singlefood_layout)
abstract class SingleFoodModel :
     EpoxyModelWithHolder<SingleFoodModel.FoodHolder>(){

    @EpoxyAttribute
    var id : Long = 0

    @EpoxyAttribute
    @DrawableRes
    var image : Int = 0

    @EpoxyAttribute
    var title:String? = ""

    @EpoxyAttribute
    var desc:String = ""

    override fun bind(holder: FoodHolder) {
        holder.imageView?.setImageResource(image)
        holder.titleView?.text = title
    }

-----------------------------------------
//방법 1
    inner class FoodHolder : EpoxyHolder(){

        lateinit var imageView:ImageView
        lateinit var titleView: TextView
        lateinit var descView:TextView

        override fun bindView(itemView: View?) {
            imageView = itemView?.image
            titleView = itemView?.title
            descView = itemView?.desc
        }
    }
    
-----------------------------------------
//방법 2
    //다른 방법입니다. KotlinHolder를 사용하면 다음을 따르면 됩니다.
    
    inner class ItemsHolder : KotlinHolder() {
        val imageView by bind<TextView>(해당 컴포넌트의 id R.id.imageView)
        val titleView by bind<TextView>(R.id.titleView)
        val descView by bind<TextView>(R.id.descView)
    }
------------------------------------------
}

Epoxy에서는 EpoxyModels에서 처리합니다. 어댑터와 마찬가지로 Epoxy Holders에서 확장된 ViewHolder 클래스도 있습니다.

위의 코드에서 볼 수 있듯이이 Model 클래스는 RecyclerView.Adapter 클래스에있는 것과 같습니다. 이 경우 FoodHolder 클래스이고 EpoxyHolder에서 파생 된 뷰 홀더 클래스를 포함합니다.

Model properties 들은 @EpoxyAttribute 어노테이션을 통해 정의됩니다.

다음은 앱에 표시할 데이터가 필요합니다. 임의의 데이터를 생성하는 데이터 팩토리를 생성합니다.


EpoxyModels 에서 사용할 KotlinHolder를 생성합니다.
EpoxyHolder를 상속받고있으며 Model의 bind를 도와줍니다.

abstract class KotlinHolder : EpoxyHolder() {

    private lateinit var view: View

    override fun bindView(itemView: View) {
        view = itemView
    }

    protected fun <V : View> bind(id: Int): ReadOnlyProperty<KotlinHolder, V> =
        Lazy { holder: KotlinHolder, prop ->
            holder.view.findViewById(id) as V?
                ?: throw IllegalStateException("View ID $id for '${prop.name}' not found.")
        }

    private class Lazy<V>(private val initializer: (KotlinHolder, KProperty<*>) -> V) :
        ReadOnlyProperty<KotlinHolder, V> {

        private object EMPTY

        private var value: Any? = EMPTY

        override fun getValue(thisRef: KotlinHolder, property: KProperty<*>): V {
            if (value == EMPTY) {
                value = initializer(thisRef, property)
            }
            @Suppress("UNCHECKED_CAST")
            return value as V
        }
    }
}

FoodDataFactory

object FoodDataFactory{

//region Random Data Generatorsprivate val random = Random()

    private val titles = arrayListOf<String>("Nachos", "Fries", "Cheese Balls", "Pizza")

    private fun randomTitle() : String {
        val title = random.nextInt(4)
        return titles[title]
    }

    private fun randomPicture() : Int{
        val grid = random.nextInt(7)

        return when(grid) {
            0 -> R.drawable.nachos1
            1 -> R.drawable.nachos2
            2 -> R.drawable.nachos3
            3 -> R.drawable.nachos4
            4 -> R.drawable.nachos5
            5 -> R.drawable.nachos6
            6 -> R.drawable.nachos7
            else -> R.drawable.nachos8
        }
    }
//endregion
fun getFoodItems(count:Int) : List<Food>{
        var foodItems = mutableListOf<Food>()
        repeat(count){
            val image = randomPicture()
            val title = randomTitle()
            @StringRes val desc = R.string.nachosDesc
            foodItems.add(Food(image,title,desc))
        }
        return foodItems
    }
}

그 다음은 controller를 만들러 가보겠습니다. EpoxyController를 상속받고 buildModels 메소드를 implement 합니다.

SingleFoodController

class SingleFoodController : EpoxyController(){

    var foodItems : List<Food>

    init {
        foodItems = FoodDataFactory.getFoodItems(50)
    }

    override fun buildModels() {
        var i:Long =0

        foodItems.forEach {food ->
            SingleFoodModel_()
                    .id(i++)
                    .image(food.image)
                    .title(food.title)
                    .addTo(this)
        }
    }

}

이 클래스는 EpoxyController 클래스에서 확장되었으며이 컨트롤러에 모델 / 모델을 추가하는 buildModels () 메서드를 재정의해야합니다.

이제 리사이클러뷰에서 생성된 기능을 설정할 준비가 되었습니다. 이를 위해 MainActivity에서 다음과 같이 코드를 작성합니다.


MainActivity

class MainActivity : AppCompatActivity() {

    private val recyclerView : RecyclerView by lazy { findViewById<RecyclerView>(R.id.recycler_view) }

private val singleFoodController : SingleFoodController by lazy
{ SingleFoodController() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initRecycler()
    }

    private fun initRecycler(){
        val linearLayoutManager = LinearLayoutManager(this)
        recyclerView.apply {
            layoutManager = linearLayoutManager
            setHasFixedSize(true)
            adapter = singleFoodController.adapter
            addItemDecoration(DividerItemDecoration(this@MainActivity, linearLayoutManager.orientation))
        }

//This statement builds model and add it to the recycler view
        singleFoodController.requestModelBuild()
    }

수직 ? 수평 ?

Recyclerview에서는 layoutmanager를 통해서 orientation을 vertical이나 horizontal로 설정할 수 있었습니다.
하지만 EpoxyRecyclerview에서는 기본적으로 linearlayoutmanger가 사용되며 레이아웃 매개변수를 기반으로 스크롤 방향이 결정이 됩니다.
Recyclerview가 match로 설정된 경우 스크롤방향은 세로, setHasFixedSize로 설정되고 true로 설정됩니다.
높이가 wrap으로 설정되면 스크롤 방향은 수평으로 설정되고 false로 설정됩니다.
EpoxyRecyclerView를 서브클래스홯여 재정의 하여 createLayoutManager고유 기본값을 설정할 수 있습니다.

하지만 이전의 recyclerview처럼 layoutmanager도 설정이 가능합니다.
xml에서 layoutmanager를 설정 가능하며 span값을 준다면 grid도 가능합니다.


이벤트

각각의 리사이클러뷰, 혹은 리스트에 클릭 리스너를 달아줄 일이 많을것이다.
리스너를 달아주는 위치는 다음과 같다.

@EpoxyModelClass(layout = R.layout.test)
abstract class TestModel(var testItem: ItemTest) :
    EpoxyModelWithHolder<TestModel.ItemsHolder>() {

    override fun bind(holder: TestHolder) {
        with(holder) {
            this.homeLabel.text = homeLabelItem.text
            this.homeLabel.setOnClickListener {
								//이곳에 클릭리스너에 대한 이벤트를 설정해준다.
            }
        }
    }

코드 분해

setDefaultGlobalSnapHelperFactory

requestModelBuild() == notifyDataSetChange()와 동일하다.
데이터가 변경될때마다 알려준다.

Delegates.observable()는 프로퍼티를 선언할 때 Observable로 만들어준다.
해당 프로퍼티의 변화를 감지하여 그 뒤에 이어지는 람다를 실행시킨다.

profile
러닝커브를 따라서 등반중입니다.
post-custom-banner

3개의 댓글

comment-user-thumbnail
2021년 10월 5일

궁금한게 있습니다 EpoxyModel 클래스의 Holder 클래스에서 따로 findViewById같은것을 진행안해줘도 되나요>

1개의 답글