IT 프로그래밍-Android

[데이터바인딩] RecyclerView와 BindingAdapter

godsangin 2020. 4. 29. 22:05
반응형

지난 시간에 이어서 데이터바인딩 실습을 진행해보도록 하겠습니다.

오늘은 저번 실습인 텍스트 붙히기 예제를 RecyclerView의 item으로 추가하는 예제를 준비해봤습니다 !

우선 RecyclerView를 사용하기 위해 gradle 설정을 추가합니다.

...
apply plugin: 'kotlin-kapt
...
dependencies{
	...
	implementation 'com.android.support:design:29.0.0'
	implementation 'com.android.support:support-v4:29.0.0'
    ...
}

 

apply plugin: 'kotlin-kapt'를 하는 이유는 이번 실습의 핵심 기능인 BindingAdapter를 사용하기 위해서 추가해줍니다(코틀린으로 bindingadapter를 사용하기 위해서 추가해 줘야 합니다).

 

작성이 완료되면 이번 실습의 내용에서 필요한 Memo라는 클래스(Model)를 생성합니다.

package com.myhome.viewmodelsample.model

data class Memo(var id:Long, var text:String)

Memo class는 코틀린의 data class를 사용하였습니다.(data class를 사용하면 toString, equals와 같은 메소드가 자동으로 오버라이드 되기 때문에 편리합니다)

 

다음으로 MainViewModel의 내용을 다음과 같이 추가합니다.

package com.myhome.viewmodelsample.viewmodel

import androidx.databinding.ObservableArrayList
import androidx.databinding.ObservableField
import com.myhome.viewmodelsample.model.Memo

class MainViewModel {
    val myText = ObservableField<String>()
    var index:Long = 0
    val memoList = ObservableArrayList<Memo>()

    fun addMemo(){
        memoList.add(Memo(index++, myText.get().toString()))
        myText.set("")
    }
}

이번에 추가된 내용은 Memo의 id를 담당할 index변수와 RecyclerView에 추가될 아이템인 memoList입니다. 또한 view의 action이벤트인 addMemo 함수도 생성해줍니다(함수가 실행되면 memoList에 Memo객체가 추가되도록 설정합니다).

 

다음은 activity_main.xml입니다.

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="model"
            type="com.myhome.viewmodelsample.viewmodel.MainViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout 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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".view.MainActivity">

        <EditText
            android:id="@+id/myEdit"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            android:layout_margin="12dp"
            android:text="@={model.myText}"/>
        <TextView
            android:id="@+id/myTv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{model.myText}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/myEdit" />

        <Button
            android:id="@+id/myBt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/myTv"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_margin="4dp"
            android:text="메모 추가"
            android:onClick="@{() -> model.addMemo()}"/>

        <androidx.recyclerview.widget.RecyclerView
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:layout_constraintTop_toBottomOf="@id/myBt"
            app:layout_constraintBottom_toBottomOf="parent"
            bind_memolist="@{model.memoList}"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

위처럼 추가가 되면 기본적인 구조는 완성입니다.

Button에 대한 이벤트리스너를 등록하는 방식은 @{(0 -> 뷰모델.함수이름()}으로 정의합니다.

다음으로 생소한 attribute인 bind_memolist가 있죠 ??

바인딩 어댑터를 사용하면 view를 정의할 때 직접 지정한 attribute를 사용할 수 있습니다.

다음은 바인딩 어댑터를 정의한 DatabindingUtils라는 클래스입니다.

package com.myhome.viewmodelsample.utils

import android.widget.TextView
import androidx.databinding.BindingAdapter
import androidx.databinding.ObservableArrayList
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.myhome.viewmodelsample.model.Memo
import com.myhome.viewmodelsample.view.RecyclerAdapter

object DatabindingUtils {
    @BindingAdapter("bind_memolist")
    @JvmStatic
    fun bindMemoList(recyclerView:RecyclerView, items:ObservableArrayList<Memo>){
        if(recyclerView.adapter == null){
            val lm = LinearLayoutManager(recyclerView.context)
            val adapter = RecyclerAdapter()
            recyclerView.layoutManager = lm
            recyclerView.adapter = adapter
        }
        (recyclerView.adapter as RecyclerAdapter).items = items
        recyclerView.adapter?.notifyDataSetChanged()
    }

    @BindingAdapter("bind_text")
    @JvmStatic
    fun bindText(textView:TextView, id:Long){
        textView.setText(id.toString())
    }
}

위의 xml에 정의된 bind_memolist라는 attribute를 위와 같이 정의할 수 있습니다(전체 클래스를 object로 정의하는 것 잊으시면 안됩니다). 여기서 볼 수 있듯이 recyclerview의 adapter를 바인딩어댑터를 통해 정의하는 것을 확인할 수 있습니다. 이로써 mainactivity인 view와 memo인 model이 분리된 것을 확인할 수 있습니다. memo의 변화는 오직 viewmodel에 정의되고 viewmodel의 변화를 view가 감지할 수 있도록 바인딩어댑터를 통해 정의할 수 있습니다.

여기서 잠깐 !! recyclerview에 adapter를 추가하기 전에 필요한 커스텀 어댑터와 item.xml이 빠졌죠 ??

다음은 RecyclerAdapter와 memo_item.xml입니다.

package com.myhome.viewmodelsample.view

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.myhome.viewmodelsample.databinding.MemoItemBinding
import com.myhome.viewmodelsample.model.Memo

class RecyclerAdapter :RecyclerView.Adapter<RecyclerAdapter.ViewHolder>(){
    var items = ArrayList<Memo>()
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = MemoItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        val holder = ViewHolder(binding)
        return holder
    }

    override fun getItemCount(): Int {
        return items.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items[position])
    }

    class ViewHolder(binding:MemoItemBinding):RecyclerView.ViewHolder(binding.root){
        val binding = binding
        fun bind(memo:Memo){
            binding.model = memo
        }
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="model"
            type="com.myhome.viewmodelsample.model.Memo" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="12dp">
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_margin="4dp"
            bind_text="@{model.id}"
            />
        <TextView
            android:id="@+id/descriptoin"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/title"
            app:layout_constraintLeft_toLeftOf="parent"
            android:layout_margin="4dp"
            android:text="@{model.text}"/>
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

평소의 recycleradapter와는 조금 다르게 뷰를 생성하고 bind하는 것을 확인할 수 있습니다. 이는 ViewHolder를 데이터바인딩을 사용하여 생성할 수 있기 때문입니다. 지난시간의 ActivityMainBinding과 같이 MemoItemBinding이라는 클래스가 memo_item.xml을 정의하면서 자동으로 generate됩니다.

예제에서는 memo라는 아이템이 간단히 보여주기 위한 목적만 존재하기 때문에 recyclerview item(view)-memo(model)의 구조로 작성하였지만 필요에 따라서(memo에 클릭이벤트, 여러 컴포넌트에 따른 동작이 필요할 경우)

recyclerview item(view) - viewmodel - memo(model)의 구조로 작성할 수도 있습니다. 이럴 경우 variable에 필요한 viewmodel을 직접 새로 정의해야합니다.

 

또한 좀전의 bindingadapter에서 정의한 bind_text라는 attibute도 확인할 수 있습니다. 이는 textview에서 setText(Long타입)이 불가능하기 때문에 직접 함수로 정의한 것입니다.

 

이와 같이 view와 model을 분리하기 위해서 bindingadapter를 사용할 수 있습니다.

다음은 프로젝트 구조입니다.

 

완성된 예제는 다음과 같습니다.