기본적인 데이터 바인딩에 대한 내용은 Data Binding - 1에 정리하였고, 이 글은 Data Binding의 표현식 일부와 Binding Adapter에 대한 내용을 다루기위해 작성하였습니다.
데이터 바인딩 라이브러리는 data 태그 안에서 import, variable, include 태그를 제공하고 있습니다.
@{}
)에 사용할 수 있는 변수(프로퍼티)를 나타냅니다.data
요소 내에서 여러 variable
를 선언할 수 있는데, XML에서 사용할 변수라고 생각하면 됩니다. 즉, variable
요소는 레이아웃 파일 내 표현식에 사용될 프로퍼티입니다. 아래는 user
, image
및 note
라는 variable을 선언한 예제입니다.
<data>
<import type="android.graphics.drawable.Drawable"/>
<variable name="user" type="com.example.User"/>
<variable name="image" type="Drawable"/>
<variable name="note" type="String"/>
</data>
생성된 바인딩 클래스에는 각 variable에 대한 setter와 getter가 정의되어 있습니다. variable에 대해서 setter가 호출되기 전까지는 default 값을 사용합니다. 예를 들어 참조 타입은 null과 같은 값을 가지고 있습니다.
최상위 태그를 <layout>
으로 지정한 데이터 바인딩이 적용된 XML에서 Context 객체에 접근할 수 있습니다. 즉 Variable로 선언하지 않고도 사용할 수 있는데, 이는 루트 뷰의 getContext() 메서드에서 가져온 Context 객체입니다.
import는 레이아웃 파일안에서 클래스에 대해 쉽게 참조할 수 있도록 도와줍니다. data
태그 내에서 선언하며, 만약 레이아웃 파일내에서 View 클래스를 참조한다고 가정하면 아래와 같이 사용할 수 있습니다. 즉, 코드에서 import를 통해 클래스를 쉽게 참조하는 것처럼, View에서도 코드와 같은 방식으로 호출하는 것입니다.
<data>
<import type="android.view.View"/>
</data>
<TextView
android:text="@{user.lastName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>
참조한 클래스는 바인딩 표현식(@{}
)안에서 위와 같이 사용할 수 있습니다. 위의 예제는 View 클래스의 상수에 해당하는 VISIBLE과 GONE을 사용하고 있습니다.
import된 타입은 변수나 표현식에서 타입 참조로 사용할 수 있습니다. 아래의 예제는 import 태그를 통해 User
및 List
를 참조하고, 이를 variable의 타입으로 사용하는 코드입니다.
<data>
<import type="com.example.User"/>
<import type="java.util.List"/>
<variable name="user" type="User"/>
<variable name="userList" type="List<User>"/>
</data>
참고로 List<>
와 같이 화살괄호가 들어가는 타입을 바인딩 표현식 안에서나 변수로 선언할 때는 화살괄호를 그대로 작성할 수 없고 <
은 <로 >
는 >로 표현하여야 합니다. 추가적인 정보는 Stack overflow - android databinding using && logical operator에 나와 있습니다.
표현식에서 정적 필드 및 메소드를 참조할 때 import한 타입을 사용할 수 있습니다. 예를 들어, 최상위 함수나 최상위 프로퍼티가 선언된 클래스 파일을 import하여 가져온 후 레이아웃 파일안에서 그 클래스를 사용하여 참조할 수 있습니다.
<!-- 최상위 함수 capitalize 함수가 선언된 파일 이름이 MyStringUtils라고 가정 -->
<data>
<import type="com.example.MyStringUtils"/>
<variable name="user" type="com.example.User"/>
</data>
…
<TextView
android:text="@{MyStringUtils.capitalize(user.lastName)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<include>
태그를 통해 하나의 XML에 작성된 내용을 다른 XML로 작성해서 포함할 수 있습니다. 단, 주의할 점이 하나 있습니다. include되고 바인딩이 사용된 xml을 sub.xml, include하는 xml을 main.xml이라고 가정한다면 단지 main.xml에서 sub.xml을 inclue한다고 바인딩 표현식에서 사용한 값들이 나타나지 않습니다. 아래와 같이 사용해야 합니다.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="model" type="com.example.Model"/>
</data>
<TextView
android:text='@{"include xml... data : " + model.name}' />
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable name="model" type="com.example.Model"/>
</data>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/sub"
bind:model="@{user}"/>
</LinearLayout>
</layout>
include에 의해 데이터 바인딩이 정상적으로 되려면 include 하는 XML(main.xml)에서 명시적으로 include 대상이 되는 XML(sub.xml)에 데이터를 넘겨줘야 하며, include 대상이 되는 XML(sub.xml) 내에도 이를 받을 준비가 되어야 합니다.
<include>
태그에 bind:model과 같이 이용할 variable을 bind:variable 형식으로 지정합니다.
바인딩 어댑터는 뷰에 개발자가 정의하는 메소드를 호출하여 값을 설정하는 작업을 담당합니다. 예를 들면 setText() 메서드를 통해 뷰의 text 값을 설정하거나, setOnClickListener() 메서드를 통해 이벤트 리스너를 설정하는 작업이 해당됩니다. 즉, 쉽게 말하자면 바인딩 어댑터는 view의 속성을 custom하게 작성하여 추가하는 것입니다. 예제를 통해 알아보겠습니다.
// DataBinding
buildFeatures { dataBinding = true }
dependencies {
...
def lifecycle_version = "2.4.0"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
}
dataBinding을 위한 부분은 buildFeatures { dataBinding = true }
이고, 아래의 예제에서 ViewModel과 LiveData도 같이 사용하기에 의존성을 추가하였습니다.
<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="kr.co.lee.databindingexample.viewmodels.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- visible 이라는 Custom 속성을 정의 -->
<TextView
app:visible="@{viewModel.isVisible}"
android:id="@+id/binding_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Binding Adapter 테스트"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- viewModel의 setVisible() 메소드를 통해 값 변경 -->
<Button
android:id="@+id/visible_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="visible or inVisible ?"
app:layout_constraintTop_toBottomOf="@id/binding_view"
android:onClick="@{() -> viewModel.setVisible()}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
<layout>
태그를 최상위 태그로 설정하고 XML 파일안에서 ViewModel에 접근하기 위해서 <variable>
로 선언합니다.
바인딩 어댑터와 관련된 부분은 TextView의 app:visible 속성입니다.
@BindingAdapter("visible")
fun visible(view: View, isVisible: Boolean) {
view.visibility = if(isVisible) View.VISIBLE else View.INVISIBLE
}
// 확장함수 형태
@BindingAdapter("visible")
fun View.visible(isVisible: Boolean) {
visibility = if(isVisible) View.VISIBLE else View.INVISIBLE
}
위의 코드는 isVisible이라는 Boolean의 매개변수 값에 따라서 View를 보이게하고 안 보이게 하는 코드입니다.
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
lateinit var mainViewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ViewModel 객체 생성
mainViewModel = ViewModelProvider(this)[MainViewModel::class.java]
// DataBinding 레이아웃 지정
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.apply {
// binding의 lifeCycle을 액티비티로 지정
lifecycleOwner = this@MainActivity
// XML의 variable로 선언한 ViewModel 객체 지정
viewModel = mainViewModel
}
}
}
class MainViewModel: ViewModel() {
private val _isVisible = MutableLiveData<Boolean>(false)
val isVisible: LiveData<Boolean> get() = _isVisible
// 버튼을 클릭하면 호출되는 메소드
fun setVisible() {
_isVisible.value = (_isVisible.value != true)
}
}
LiveData<Boolean>
타입입니다.정리하자면 BindingAdapter의 구조와 정의 아래와 같습니다.
기본적으로 데이터 바인딩은 단방향 바인딩입니다. 단방향 바인딩이란 데이터 모델(주로 LiveData 등)의 값이 변경되면 연결된 레이아웃에 해당 값을 변경해주지만 레이아웃의 값이 변경되면 데이터 모델은 변경되지 않고 레이아웃의 값만 변경됩니다. 반면 양방향 바인딩은 기본적으로 단방향 바인딩의 기능을 가지고 있고 그에 더해 레이아웃의 값이 바뀌면 연결된 데이터 모델의 값도 변경됩니다.
간단한 예제를 확인해보겠습니다.
class MainViewModel : ViewModel() {
val checkFlag = MutableLiveData(false)
}
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
viewModel = ViewModelProvider(this)[MainViewModel::class.java]
binding.apply {
vm = viewModel
lifecycleOwner = this@MainActivity
}
}
}
<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="vm"
type="com.example.samplecode.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<CheckBox
android:text="Check test"
android:checked="@={vm.checkFlag}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/label_check"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/label_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{vm.checkFlag.toString()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
양방향 바인딩은 일반적인 바인딩 표현식과는 다르게 (@={}
)과 같이 사용합니다. 현재는 CheckBox의 checked에 양반향 바인딩을 설정하였기에 check 상태가 되면 ViewModel의 checkFlag 값이 true로 바뀌고, uncheck 상태가 되면 checkFlag 값이 false로 바뀌게 됩니다.
참조
안드로이드에서 DataBinding에 BindingAdapter 사용법
데이터바인딩 two-way binding 원리
안드로이드 developer - Binding adapters
안드로이드 ViewModel
안드로이드 LiveData
안드로이드 DataBinding
틀린 부분은 댓글로 남겨주시면 수정하겠습니다..!!