RecyclerView를 구현하기 위해 Adapter를 만들어야 하는데, 아이템의 특성상 UI와 클릭 이벤트 등 Adapter를 매번 다르게 구현해야 한다는 단점이 있습니다. 이번 글에서는 Binding을 활용하여 Adapter를 하나만 만들어 여러 RecyclerView에 재사용 해보겠습니다.
BindingViewHolder.kt
class BindingViewHolder (
val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root)
Adapter에서 Binding에 사용할 BindingViewHolder를 만들어줍니다.
data class RecyclerItem(
val viewModel: Any,
val navigator: Any,
@LayoutRes val layoutId: Int
) {
fun bind(binding: ViewDataBinding) {
binding.setVariable(BR.viewModel, viewModel)
binding.setVariable(BR.navigator, navigator)
}
}
각각의 다른 객체를 공통적으로 사용하기 위해 하나로 묶어줄 RecyclerItem을 만들어줍니다.
viewModel은 RecyclerView 아이템에 데이터를 바인딩을 하기 위해 파라미터로 받고,
navigator는 RecyclerView 아이템의 UI 이벤트를 처리하기 위해 받습니다.
class RecyclerViewAdapter : RecyclerView.Adapter<BindingViewHolder>() {
private val itemList = mutableListOf<RecyclerItem>()
fun updateItem(newItems: List<RecyclerItem>) {
this.itemList.clear()
this.itemList.addAll(newItems)
this.notifyDataSetChanged()
}
override fun getItemCount(): Int {
return itemList.size
}
override fun getItemViewType(position: Int): Int {
return itemList[position].layoutId
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding = DataBindingUtil.inflate(inflater, viewType, parent, false)
return BindingViewHolder(binding)
}
override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
itemList[position].bind(holder.binding)
holder.binding.executePendingBindings()
}
}
@BindingAdapter("recyclerItems")
fun RecyclerView.setRecyclerViewItems(
items: List<RecyclerItem>?
) {
if (adapter == null) {
adapter = RecyclerViewAdapter()
layoutManager = LinearLayoutManager(context)
}
items?.let { (adapter as? RecyclerViewAdapter)?.updateItem(it) }
}
BindingAdapter로 XML의 RcyclerView에 바인딩 하기 위한 속성을 만들어줍니다.
위 코드를 활용하여 메모를 RecyclerView로 간단히 띄워보겠습니다.
Note.kt
data class Note(
val title : String?,
val content : String?,
val date : String?
)
NoteItemViewModel.kt
class NoteItemViewModel(val note: Note) {
val title = MutableLiveData<String>()
val date = MutableLiveData<String>()
init {
title.value = note.title
date.value = note.date
}
}
아이템에 데이터를 바인딩해줄 ItemViewModel을 만들어줍니다.
NoteItemNavigator.kt
interface NoteItemNavigator {
fun onClickItem(item: Note)
}
XML에서 클릭 이벤트를 받아 ViewModel에서 구현하기 위해 인터페이스를 만들어줍니다.
item_note.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="viewModel"
type="com.sample.note.viewmodel.NoteItemViewModel" />
<variable
name="navigator"
type="com.sample.note.navigator.NoteItemNavigator" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="100dp">
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:onClick="@{() -> navigator.onClickItem(viewModel.note)}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginBottom="5dp"
android:text="@{viewModel.title}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:text="@{viewModel.date}" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
아이템의 레이아웃 XML 코드입니다.
위 레이아웃은 viewModel로 TextView에 데이터를 바인딩하고, navigator로 클릭 이벤트를 처리하도록 구현하였습니다.
MainViewModel.kt
class MainViewModel: ViewModel(), NoteItemNavigator {
val noteItemList = MutableLiveData<ArrayList<RecyclerItem>>()
private fun getAllNote() {
...
override fun onSuccess(t: List<Note>) {
noteItemList.value = ArrayList(t.toRecyclerItemList())
}
...
}
fun List<Note>.toRecyclerItemList() = map {
NoteItemViewModel(it).toRecyclerItem()
}
fun NoteItemViewModel.toRecyclerItem() = RecyclerItem(
viewModel = this,
navigator = this@MainViewModel,
layoutId = R.layout.item_note
)
override fun onClickItem(item: Note) {
// ...
}
서버나 내부 DB로부터 데이터 리스트를 받은 후 ItemViewModel로 viewModel과 navigator, layoutId를 넘겨줍니다.
그리고 Navigator를 이용하여 Adapter가 아닌 ViewModel에서 클릭 이벤트를 처리해줄 수 있습니다.
activity_main.xml
...
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:recyclerItems="@{viewModel.noteItemList}" />
...
이런 형식으로 BindingAdapter 속성을 이용하여 viewModel의 itemList를 바인딩 해줍니다.
RecyclerView의 UI나 기능이 달라질 때마다 매번 Adapter를 만들어줘야 했지만, 이 방식을 사용한 후로부터 중복적인 코드를 많이 줄일 수 있었습니다. 조금 복잡해보일 수 있지만, 확실히 Adapter의 개수가 늘어날수록 효율적으로 느껴졌습니다.