[Android] RecyclerView Adapter 재사용

jini0318·2021년 1월 22일
0

Android

목록 보기
6/7
post-thumbnail

RecyclerView의 단점

RecyclerView를 구현하기 위해 Adapter를 만들어야 하는데, 아이템의 특성상 UI와 클릭 이벤트 등 Adapter를 매번 다르게 구현해야 한다는 단점이 있습니다. 이번 글에서는 Binding을 활용하여 Adapter를 하나만 만들어 여러 RecyclerView에 재사용 해보겠습니다.

재사용 가능한 Adapter 코드

BindingViewHolder 생성

BindingViewHolder.kt

class BindingViewHolder (
    val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root)

Adapter에서 Binding에 사용할 BindingViewHolder를 만들어줍니다.

RecyclerItem 생성

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 이벤트를 처리하기 위해 받습니다.

RecyclerViewAdapter 생성

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 사용

@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의 개수가 늘어날수록 효율적으로 느껴졌습니다.

profile
Park Jin

0개의 댓글