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을 만들겠습니다.
data class Food (
val image:Int=-1,
val title:String="",
val description:String=""
)
다음은 EpoxyModelWithHolder를 상속받는 뷰홀더 모델을 만듭니다.
특정 뷰를 뷰홀더에 바인딩 한 다음 데이터를 전달하기 위해 RecyclerView.Adapter에서 확장 된 어댑터가 필요합니다.
@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
}
}
}
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 합니다.
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에서 다음과 같이 코드를 작성합니다.
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로 만들어준다.
해당 프로퍼티의 변화를 감지하여 그 뒤에 이어지는 람다를 실행시킨다.
궁금한게 있습니다 EpoxyModel 클래스의 Holder 클래스에서 따로 findViewById같은것을 진행안해줘도 되나요>