Android

BindingAdapter에 대해서

최데브 2022. 3. 27. 23:55

Databinding 관련 작업을 하다보면 BindingAdapter에 대해서 자주 보게 된다.

오늘은 BindingAdapter를 작업 할 일이 있었는데 생각이 난김에 정리를 해보려고 한다.

 

먼저 BindingAdapter는 뭐고 왜 쓰는걸까?

BindingAdapter 는 view 의 속성을 커스텀하게 추가시킬 수 있는 것이다.

 

일단 잡다하게 BindingAdapter 을 적용하기 위해서 필요한 선작업들은 알고 있다는 상황으로 가정하고 적겠다.

 

먼저 viewmodel 부터 보자.

@HiltViewModel
class FeedViewModel
@Inject constructor (private val feedUsecase: FeedReposUseCase): ViewModel() {

    private val _spinnerEntry = MutableStateFlow(emptyList<String>())
    val spinnerEntry : StateFlow<List<String>?> = _spinnerEntry

     val spinnerData = MutableStateFlow<String>("")


    fun setSpinnerEntry(Entry: List<String>) {
        viewModelScope.launch {
            _spinnerEntry.emit(Entry)
        }
    }

}

스피너 관련 작업을 했었는데 여기서 spinnerEntry 를 기억해두자.

 

다음은 액티비티의 layout xml 파일이다.

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

    <data>
        <variable
            name="viewmodel"
            type="com.dev6.feed.viewmodel.FeedViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.dev6.feed.FeedActivity">


    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/constraintLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintVertical_bias="0"
        app:layout_constraintHeight_percent="0.1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <Spinner
            android:id="@+id/locationSpinner"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:entries="@{viewmodel.spinnerEntry}"
            tools:selectedValue="@={viewmodel.spinnerData}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent">
        </Spinner>
    </androidx.constraintlayout.widget.ConstraintLayout>

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

여기서 <data> 라는 태그안에 viewmodel 로 이름을 정하고 위에 적었던 FeedViewmodel 을 적어뒀다.

그리고 Spinner 안을 보면 viewmodel.spinnerEntry 라고 불러오는게 보인다.

이 말은 FeedViewmodel 에 선언 되어있는  spinnerEntry 라는 변수를 난 Spinner 에서 사용할거야 라는 뜻이다.

 

이렇게 적어주면 activity 즉 UI 부분에서 데이터를 set 해주는 코드를 따로 적지 않고 view 와 직접 바인딩 시킨다.

훨씬 깔끔하고 직관적으로 바뀌게 된다. 이게 기본적인 databinding 의 개념이다.

 

@AndroidEntryPoint
class FeedActivity : BindingActivity<ActivityFeedBinding>(R.layout.activity_feed) {

    private  val feedViewModel : FeedViewModel by viewModels()
    var list  = listOf("zz" , "zz2")

    override fun initView() {
        super.initView()

    }

    override fun initViewModel() {
        super.initViewModel()
        binding.viewmodel = feedViewModel
    }

    override fun afterOnCreate() {
        super.afterOnCreate()
        feedViewModel.setSpinnerEntry(list)
        lifecycleScope.launch {
            feedViewModel.spinnerData.collect{
                Log.v("sdfsdfsdf" , it)
            }
        }

    }
}

그리고 액티비티 쪽에선 이렇게만 사용해주고 있다. 

의문점이 하나 생긴다.

어? spinner를 생성해주는 코드는 하나도 없는데 이게 데이터가 들어가고 spinner가 정상적으로 작동해?

맞다 사실 이 코드만 있으면 제대로 될리가 없다.

위에서 설명을 안했지만 xml의 Spinner 안에 tools:entries 이런게 있었다. 이건 뭘까.

 

이때 BindingAdpater 이 등장한다.

object LocationAdapter {
    @JvmStatic
    @BindingAdapter("entries")
    fun Spinner.setEntries(entries: List<String>?) {
        entries?.run {
            val arrayAdapter = ArrayAdapter(context, R.layout.support_simple_spinner_dropdown_item, entries)
            arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
            adapter = arrayAdapter
        }
    }


    @JvmStatic
    @BindingAdapter("selectedValue")
    fun Spinner.setSelectedValue(selectedValue: String) {
        adapter?.run {
            val position =
                (adapter as ArrayAdapter<Any>).getPosition(selectedValue)
            setSelection(position, false)
            tag = position
        }
    }


// attribute는 바인딩 어뎁터처럼 value을 의미한다. event는 반응할 bindingapdater의 value를 의미한다.
    @JvmStatic
    @InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged")
    fun Spinner.getSelectedValue(): Any? {
        return selectedItem
    }


    // 위에것이 실행되고 아래의 bindingapdater가 실행된다.
    @JvmStatic
    @BindingAdapter("selectedValueAttrChanged")
    fun Spinner.setInverseBindingListener(inverseBindingListener: InverseBindingListener?) {

        inverseBindingListener?.run {
            onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
                override fun onItemSelected(
                    parent: AdapterView<*>,
                    view: View,
                    position: Int,
                    id: Long
                ) {
                    if (tag != position) {
                        inverseBindingListener.onChange()
                    }
                }

                override fun onNothingSelected(parent: AdapterView<*>) {}
            }
        }
    }


}

이 코드의 젤 위를보자.

    @JvmStatic
    @BindingAdapter("entries") 가 보인다.

JvmStatic 는 static 으로 해당 코드가 사용될 수 있게 만들어주는 어노테이션이다.

그리고 오늘의 주인공 @BindingAdapter 이 나온다. ( ) 안의 이름이 xml 에서 불러오는 속성값의 이름이 된다.

그래서  우리는 xml 에서 tools:entries 이렇게 불러올 수 있게 되는것이다.

그리고 이렇게 불러왔을때 

 

    fun Spinner.setEntries(entries: List<String>?) {
        entries?.run {
            val arrayAdapter = ArrayAdapter(context, R.layout.support_simple_spinner_dropdown_item, entries)
            arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
            adapter = arrayAdapter
        }
    }

이렇게 커스텀된 코드가 작동하게 되는것이다.

그럼 위에서 spinner 를 만들어주는 코드가 activity에 없는데? 라는 의문이 사라질것이다.

우리는 bindingAdapter 로 spinner 를 생성하는 코드를 속성으로 넣어줬으니까.

 

그리고 fun Spinner.setEntries(entries: List?)  에서 파라미터로 넘어가는 entries 는 

tools:entries="@{viewmodel.spinnerEntry} 에 있는 viewmodel.spinnerEntry 의 값이 된다.

spinnerEntry 는 현재 flow로 만들어져있기 때문에 emit 으로 방출을 해야 값이 발생한다.

그래서 viewmodel 에 setSpinnerEntry 라는 함수가 있는것이다.

이걸 해주지 않으면 spinner에는 아무런 값도 들어가있지 않게 된다.

 

일단 기본적인 BindingAdapter 에 대한 설명은 이걸로 마치겠다.

 

다음 포스팅은 아직 설명하지 않은 InverseBindingAdapter 에 대해서 적어보려한다.

https://choi-dev.tistory.com/171

 

InverseBindingAdapter 에 대해

이전 포스팅에서는 BindingAdapter에 대해서 알아봤다. InverseBindingAdapter 에 대해 언급을 했었는데 이번엔 InverseBindingAdapter 에 대해 집중해서 포스팅한다. InverseBindingAdapter 은 양방향 데이터 결..

choi-dev.tistory.com

 

반응형