[안드로이드] BindingAdapter!! 두둥등장!!
728x90
반응형

 

최근에 Koin을 다시 공부하면서 샘플 코드에 RecyclerView를 사용했었다. 그 때 DataBinding도 사용했는데 "DataBinding과 RecyclerView를 모두 사용할 것이면 BindingAdapter도 사용하는게 더 낫지 않을까?"라는 생각을 했었다. (물론 RecyclerView와 BindingAdapter는 직접적으로 관계는 없다.) 그래서... BindingAdapter 두둥 등장!!! (아래 샘플 코드는 필자의 github를 참고바란다.)

 

🤔 BindingAdapter ?

BindingAdapter는 속성값을 설정하거나 이벤트리스너를 설정하는 것처럼 View(xml 레이아웃)의 attribute를 정의하고 로직을 작성하는데 사용된다. Android의 기본 UI들은 대부분 이미 정의된 BindingAdapter가 있다. 정의로 이해하기 어려울 수 있으니 바로 실습을 해 보자.

 

 

🛠 Setting

// build.gradle (app)

// 예전에는 이렇게 사용했는데
dataBinding {
    enabled = true
}

// 요즘은 이렇게 사용한다
buildFeatures {
    dataBinding = true
}

혹시나 아래와 같은 에러 메시지가 나타난다면 위 부분을 수정해보길 바란다.

DSL element 'android.dataBinding.enabled' is obsolete and has been replaced with 'android.buildFeatures.dataBinding'.

 

 

Data Binding layout으로 전환을 해보자. DataBinding 사용시, xml에는 최상위에 반드시 <layout> 태그를 사용해야 한다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        // 생략

    </LinearLayout>
</layout>

 

필자의 샘플에는 BaseKotlinActivity라는 Base 코드에 선언한 binding 객체를 초기화하는 코드가 있다. 하지만 Base 코드가 별로도 없는 경우에는 아래 두번째 코드를 참고하자.

// Base 코드가 있는 경우 - BaseKotlinActivity.kt

abstract class BaseKotlinActivity<T : ViewDataBinding, R : BaseKotlinViewModel> :
    AppCompatActivity() {

    lateinit var binding: T

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, layoutResourceId)

    }

    // 일부 생략
}
// Base 코드가 없는 경우 - MainActivity.kt

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    
    // 일부 생략
}

 

여기까지 DataBinding을 사용하기 위한 기본적인 세팅이다. BindingAdapter를 사용하려면 DataBinding을 사용하기 위한 세팅이 되어 있어야 한다.

 

 

폭풍 스크롤에도 강한 리스트를 만들어보자. BindingAdapter으로

🚗 DataBinding on View

binding은 레이아웃을 가리키는 변수이다. 이 변수를 통해 두가지 초기화를 진행한다. 첫번째는 LifecycleOwner이다. binding 변수의 생명주기를 액티비티나 프래그먼트의 생명주기에 종속시키기 위해 binding 선언 후 LifecycleOwner도 초기화를 해줄 필요가 있다. binding 변수가 View의 생명주기에 맞게 생성 및 해제가 되기 때문에 메모리 누수의 문제가 적어진다. 두번째는 ViewModel이다. 레이아웃에서 ViewModel을 선언하여 사용한다. 그러므로 액티비티에서는 초기화를 해줄 필요가 있다.

// activity_main.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="model"
            type="what.the.mvvm.viewmodel.MainViewModel" />
    </data>
    
    // 생략
// MainActivity.kt
override val viewModel: MainViewModel by viewModel()

override fun initStartView() {
    binding.lifecycleOwner = this
    binding.model = viewModel
    // 이하 생략
}

만약 LifecycleOwner 초기화가 제대로 진행되지 않을 경우, BindingAdapter 사용할 때 제대로 접근하지 못할 수 있으니 주의하자. (MutableLiveData with BindingAdapter not updating visibility of view)

 

BindingAdapter를 사용할 준비는 완료되었다. 이제 레이아웃의 뷰를 끌어다 사용해보자. 접근하고자 하는 뷰의 ID는 snake_case에서 camelCase로 바뀐다는 점에 유의하자.

// 레이아웃
<EditText
    android:id="@+id/main_activity_search_text_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:ems="10"
    android:hint="Query"
    android:inputType="textPersonName" />

// 액티비티
binding.mainActivitySearchTextView.setText("first preview of Android 11")

 

 

DataBinding과 관계는 없지만... 뒤에서 BindingAdapter를 사용하기 전에 RecyclerView와 관련된 세팅이 완료되어야 한다. 바로 MainActivity에서 Adapter가 초기화.

private val mainSearchRecyclerViewAdapter: MainSearchRecyclerViewAdapter by inject()

lateinit var imm: InputMethodManager

override fun initStartView() {
    // 일부 생략

    imm = getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as InputMethodManager
    
    binding.mainActivitySearchRecyclerView.run {
        adapter = mainSearchRecyclerViewAdapter
        layoutManager = StaggeredGridLayoutManager(3, 1).apply {
            gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
            orientation = StaggeredGridLayoutManager.VERTICAL
        }
        setHasFixedSize(true)
    }
}

 

 

 

🚀 BindingAdapter 적용

BindingAdapter 함수를 만들자. RecyclerView와 ArrayList를 인자로 받아서 RecyclerView에 데이터를 넣고 갱신하는 함수다. 그리고 레이아웃의 RecyclerView에서 이 함수를 사용하자.

@BindingAdapter("bindData")
fun bindRecyclerView(
    recyclerView: RecyclerView?,
    data: ArrayList<Document>?
) {
    val adapter = recyclerView?.adapter as MainSearchRecyclerViewAdapter
    data?.forEach {
        adapter.addPersonItem(it.doc_url)
    }
    adapter.notifyDataSetChanged()
}
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/main_activity_search_recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingTop="12dp"
    android:paddingBottom="12dp"
    bindData="@{model.personData}"/>

 

리스트로 나타날 데이터를 레이아웃에 출력하기 위해 해당 데이터(여기서는 Document)를 ViewModel에 담는다. 뷰모델(MainViewModel)의 LiveData가 데이터 변경을 감지하면 bindData가 작동한다. 감지하기 위한 LiveData는 아래와 같이 작성한다.

// MainViewModel.kt
private val _personData = MutableLiveData<ArrayList<Document>>()

val personData: LiveData<ArrayList<Document>>
    get() = _personData

 

 

 

 

🤪 부록 : Required DataBindingComponent is null in class ActivityMainBindingImpl

java.lang.RuntimeException: Unable to start activity ComponentInfo{what.the.mvvm/what.the.mvvm.MainActivity}: java.lang.IllegalStateException: Required DataBindingComponent is null in class ActivityMainBindingImpl. A BindingAdapter in what.the.mvvm.MainActivity is not static and requires an object to use, retrieved from the DataBindingComponent. If you don't use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static.

위와 같은 에러가 나타날 경우가 있다. BindingAdapter 어노테이션을 사용한 함수의 경우 static으로 접근이 가능해야 한다. @JvmStatic을 사용하거나 아래와 같이 사용하면 된다. 코틀린에서는 클래스 내부가 아닌 바로 함수를 사용하게 될 경우, static으로 선언한 것이 된다.

 

@BindingAdapter("bindData")
fun bindRecyclerView(
    recyclerView: RecyclerView?,
    data: ArrayList<Document>?
) {
    // 생략
}

 

이로써 Model(ArrayList<Document>)의 변화는 오직 ViewModel이 감지하여 정의하고, ViewModel의 변화는 View에 반영하는 모습을 볼 수 있다.

 

 

 

참고자료
- 뷰모델에서 XML로 데이터 바로 꽂기
- LiveData + ViewModel 사용해보기
- 안드로이드 BindingAdapter를 사용해보자
- 데이터바인딩 Recyclerview
- [데이터바인딩] RecyclerView와 BindingAdapter

 

그건 그렇고... 이거 너무 커엽하지 않음???

728x90
반응형