Android에서 복잡한 뷰를 쉽게 개발할 수 있게 해주는 Airbnb의 Epoxy 라이브러리
Epoxy
- Airbnb에서 만든 오픈소스 라이브러리
- 여러 뷰 타입을 갖는 RecyclerView에서 효율적으로 사용 가능, 이를 구현하는데 필요한 boilerplate code를 제거
- 디자인 변경에 빠르게 적응하고코드를 모듈화하고 관리하기 쉽게 만들기 위한 많은 인터페이스를 제공
예를 들어 위의 화면들과 같이 3개의 각각 다른 뷰타 입을 가지는 화면이 있다.
이를 RecyclerView에서 구현하려면 다음과 같은 것들이 필요
RecyclerView Object
RecyclerView.ViewHolder
RecyclerView.Adapter
Epoxy를 사용하면 아래의 컴포넌트들로 구현할 수 있다.
- Epoxy Models - 본질적으로 RecyclerView.ViewHolder 역할, RecyclerView에서 뷰가 어떻게 보이는지 설명
- Epoxy Controllers - RecyclerView에 추가될 Model을 제어하고 Epoxy Models 또는 ViewHolder를 RecyclerView에 추가할 수 있는 plug in-out 인터페이스를 제공하여 다양한 화면을 빠르고 쉽게 개발가능
build.gradle 종속성 추가
plugins {
id 'kotlin-kapt'
}
//Epoxy 추가
kapt {
correctErrorTypes = true
}
dependencies {
// Airbnb Epoxy
implementation "com.airbnb.android:epoxy:3.9.0"
kapt "com.airbnb.android:epoxy-processor:3.9.0"
}
data class 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
}
inner class FoodHolder : EpoxyHolder(){
lateinit var imageView:ImageView
lateinit var titleView: TextView
lateinit var descView:TextView
override fun bindView(itemView: View) {
imageView = itemView.findViewById(R.id.epoxy_image) //자신의 item xml의 id 값 set
titleView = itemView.findViewById(R.id.epoxy_title)
descView = itemView.findViewById(R.id.epoxy_desc)
}
}
}
Epoxy에서는 EpoxyModels에서 처리
어댑터와 마찬가지로 Epoxy Holders에서 확장된 ViewHolder 클래스도 있다.
위의 코드에서 볼 수 있듯이 Model 클래스는 RecyclerView.Adapter 클래스에 있는 것과 같다.
이 경우 FoodHolder 클래스이고 EpoxyHolder에서 파생된 뷰 홀더 클래스를 포함
Model properties 들은 @EpoxyAttribute 어노테이션을 통해 정의
다음은 앱에 표시할 데이터가 필요. 임시 데이터를 생성하는 데이터 팩토리를 생성
object FoodDataFactory{
//랜덤 생성
private 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
}
}
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() 메서드를 재정의해야 한다.
buildModels() 안에 생성한 임시 데이터로 for문으로 만들어둔 SingleFoodModel 각 값을 넣고 추가
이제 RecyclerView에서 생성된 기능을 설정할 준비가 됐다.
class EpoxyActivity : AppCompatActivity() {
lateinit var binding: ActivityEpoxyBinding
private val testController: SingleFoodController by lazy { SingleFoodController() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_epoxy)
initRecycler()
}
private fun initRecycler() {
val linearLayoutManager = LinearLayoutManager(this)
binding.epoxyRecycler.apply {
layoutManager = linearLayoutManager
setHasFixedSize(true)
adapter = testController.adapter
addItemDecoration(DividerItemDecoration(this@EpoxyActivity, linearLayoutManager.orientation))
}
testController.requestModelBuild()
}
}
여기까지는 아직 굳이 이걸 써야 하나 싶었는데 DataBinding을 적용하고 나니 코드가 정말 간단해졌다.
만들어주었던 EpoxyController와 EpoxyModelWithHolder을 다 지우고도 사용할 수 있기 때문
build.gradle 종속성 EpoxyDatabinding 추가
dependencies {
// Airbnb Epoxy
implementation "com.airbnb.android:epoxy:3.9.0"
kapt "com.airbnb.android:epoxy-processor:3.9.0"
// Epoxy Databinding
implementation "com.airbnb.android:epoxy-databinding:3.9.0"
}
@EpoxyDataBindingPattern(rClass = R.class, layoutPrefix = "epoxy") //layoutPrefix 값에 지정해줄 layout의 이름 필요
package com.example.androidtechnote; //프로젝트 package 넣어줘야함
import com.airbnb.epoxy.EpoxyDataBindingPattern;
//Epoxy DataBinding 하기 위한 package Java Class
package-info.java안에 위 값을 넣고 자신의 package를 넣어준 후 layoutPrefix 값은 원하는 layout이름으로 변경하면 된다.
‘epoxy’라고 지정해 줄 시 epoxy_item_example.xml 혹은 epoxy_item_header.xml 같이 epoxy로 시작하는 layout 파일이 생성되면 자동으로 Epoxy용 DataBindingModel이 생성
Item으로 사용될 xml에 만들어 준 DataClass 구성으로 Databinding 추가
epoxy_singlefood_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="food"
type="com.example.androidtechnote.recycler.epoxy.model.Food" />
<variable
name="onClickItem"
type="android.view.View.OnClickListener" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardCornerRadius="16dp"
android:onClick="@{onClickItem}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/epoxy_image"
android:layout_width="66dp"
android:layout_height="0dp"
android:gravity="center"
android:padding="4dp"
android:src="@{food.image}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_baseline_android_24" />
<TextView
android:id="@+id/epoxy_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@{food.title}"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/epoxy_desc"
app:layout_constraintStart_toEndOf="@+id/epoxy_image"
app:layout_constraintTop_toTopOf="parent"
tools:text="Epoxy Test" />
<TextView
android:id="@+id/epoxy_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:text="@{food.description}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/epoxy_title"
app:layout_constraintTop_toBottomOf="@+id/epoxy_title"
tools:text="Epoxy Test"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
변경사항 적용한 Activity
class EpoxyActivity : AppCompatActivity() {
lateinit var binding: ActivityEpoxyBinding
private val testController: SingleFoodController by lazy { SingleFoodController() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_epoxy)
initRecycler()
}
private fun initRecycler() {
val linearLayoutManager = LinearLayoutManager(this)
//EpoxyDataBinding 으로 구성
val dataList = FoodDataFactory.getFoodItems(50)
binding.epoxyRecycler.apply {
layoutManager = linearLayoutManager
setHasFixedSize(true)
addItemDecoration(DividerItemDecoration(this@EpoxyActivity, linearLayoutManager.orientation))
//EpoxyRecyclerView 를 찾아서 withModels 를 사용하면 별도의 epoxyController 생성없이 EpoxyRecyclerView 에 모델을 추가가능
withModels {
dataList.forEachIndexed { index, forEachFood ->
//epoxy_singlefood_layout.xml 바로사용
singlefoodLayout {
id(index)
food(forEachFood)
onClickItem { model, parentView, clickedView, position ->
Log.d("Epoxy", "$position Click")
}
}
//다른 epoxy로 시작하는 Layout도 추가 사용 가능
}
}
} //binding.epoxyRecycler.apply
}
}
여러 Header랑 다른 Item들이 필요하면 if문으로 분기한 후 xml을 바로 사용하면 MultiType RecyclerView도 쉽게 구성 가능
이렇게 Adater와 ViewHolder 자체를 사용하지 않고 RecyclerView를 구성할 수 있다.
Epoxy
https://medium.com/android-news/simplifying-recycler-view-with-epoxy-in-kotlin-nachos-tutorial-series-946d22116d57
Epoxy DataBinding
https://pish11010.medium.com/android-epoxy에-databinding-적용하기-5a213ccb538c
'Android > Library' 카테고리의 다른 글
GeoCoding 위도 경도 <-> 주소 변환하기 (4) | 2022.02.20 |
---|---|
Google Map API 사용 방법 및 예제 (0) | 2022.02.18 |
Android Room Database / 룸 데이터 베이스 (0) | 2022.01.14 |
Navigation (네비게이션) / Jetpack (0) | 2021.08.30 |
Hilt로 의존성 주입 (DI/Dependency Injection) (0) | 2021.08.30 |