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
'Android' 카테고리의 다른 글
Jetpack navigation startDestination 동적으로 설정 (0) | 2022.08.27 |
---|---|
안드로이드 Paging 3 (0) | 2022.07.24 |
MVVM 에서의 에러처리 전략 (0) | 2022.07.19 |
InverseBindingAdapter 에 대해 (0) | 2022.03.28 |
Android Databinding (데이터 바인딩) (0) | 2022.02.26 |
안드로이드 MVVM에서 코루틴 Flow로 이벤트를 처리하는 방법에 대해 (0) | 2022.02.21 |
안드로이드 클린 아키텍쳐에 대해 (0) | 2022.02.21 |
Android 프로젝트를 Multi Module 로 구성해보자. (0) | 2022.02.10 |