안드로이드 커스텀뷰 양방향 데이터바인딩
첫 회사에 일하게되면서 데이터바인딩(DataBinding)을 처음 알게되었다.
안드로이드 문서를 열심히 읽고 CodeLab도 따라해보니 이제는 익숙해졌다.
하지만 회사 전임자가 작성한 코드를 보면 커스텀뷰를 만들어서 데이터바인딩을 사용하는 경우를 본 적이 있다.
회사에 입사한지 1년 3개월이 지나서야 CustomView를 사용했을 때 데이터바인딩을 하는 방법을 공부했다. ㅎㅎ
그럼 같이 따라해보길 바란다,
1. Dependency 추가
프로젝트 수준 build.gradle에 kotln-kapt를 추가한다.
그래야지 데이터바인딩에 사용하는 어노테이션을 에러 없이 사용가능하다,
plugins {
id 'com.android.application' version '7.2.1' apply false
id 'com.android.library' version '7.2.1' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id "org.jetbrains.kotlin.kapt" version "1.6.10" apply false //요놈 추가
}
앱 수준 build.gradle에도 kotlin-kapt를 추가한다
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt' //요놈 추가
}
android {
...
buildFeatures { //이놈들 추가
dataBinding true
}
}
dependencies {
implementation 'androidx.activity:activity-ktx:1.6.0' //뷰모델 만들 때 필요
}
2. CustomView xml(레이아웃) 만들기
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/emojiTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<EditText
android:id="@+id/mainEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/emojiTextView"
app:layout_constraintTop_toTopOf="@id/emojiTextView"
app:layout_constraintBottom_toBottomOf="@id/emojiTextView"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
3. CustomView 클래스 만들기
class CustomEditText(context: Context, attributeSet: AttributeSet? = null) : FrameLayout(context, attributeSet) {
private lateinit var binding : CustomEdittextBinding
val editText //1
get() = binding.mainEditText
init {
binding = CustomEdittextBinding.inflate(LayoutInflater.from(context), this, true)
binding.emojiTextView.text = "\uD83D\uDE00"
}
}
1) 커스텀뷰인 CustomEditText에 양방향 데이터바인딩 기능을 사용해야하기 때문에 editText를 참조한다.
4. 뷰모델 만들기
class MainViewModel : ViewModel() {
val content = MutableLiveData("") //1
val TAG = MainViewModel::class.java.simpleName
init {
Log.d(TAG, "MainViewModel init")
}
}
MainActivity에서 사용할 MainViewModel 클래스를 만들었다.
1) 이 MutableLiveData를 CustomEditText와 양방향 데이터 바인딩을 할 때 사용할 것이다.
5. activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<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"
xmlns:bjs="http://schemas.android.com/apk/res-auto"
>
<data>
<variable
name="mainViewModel"
type="com.antwhale.sample.customdatabinding.MainViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.antwhale.sample.customdatabinding.CustomEditText
android:layout_width="0dp"
android:layout_height="wrap_content"
app:content="@={mainViewModel.content}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
app:content="@{mainViewModel.content}" 가 빨간줄일 것이다.
아직 데이터바인딩 속성을 정의하지 않아서 그렇다.
좀만 기다리자.
6. MainActivity.class
class MainActivity : AppCompatActivity() {
val TAG = MainActivity::class.java.simpleName
private lateinit var binding: ActivityMainBinding //1
val mainViewModel : MainViewModel by viewModels() //2
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.mainViewModel = mainViewModel //3
mainViewModel.content.observe(this) { //4
Log.d(TAG, "content: $it")
}
}
}
1) MainActivity도 데이터바인딩 사용
2) MainViewModel 선언 및 초기화
3) xml에 MainViewModel 전달
4) MainViewModel의 MutableLiveData 관찰 (CustomEditText의 값을 관찰하게된다.)
7. BindingAdapter 정의
BindingAdapter.class 파일을 만들어서 BindingAdapter의 속성들을 정의해주었다.
이때, 모든 fun은 전역으로 정의되어야한다.
@BindingAdapter("content") //1
fun setContentOnEditText(view: CustomEditText, newValue: String) {
val oldValue = view.editText.text.toString()
if(oldValue != newValue) { //2
view.editText.setText(newValue)
}
}
@BindingAdapter("contentAttrChanged") //3
fun setInverseBindingListenerOnEditText(view: CustomEditText, listener: InverseBindingListener?) {
val watcher = object : TextWatcher { //4
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun afterTextChanged(p0: Editable?) {
listener?.onChange() //5
}
}
view.editText.addTextChangedListener(watcher) //6
}
@InverseBindingAdapter(attribute = "content", event = "contentAttrChanged") //7
fun getContentOnEditText(view: CustomEditText) = view.editText.text.toString()
가장 중요한 CustomEditText의 바인딩 어댑터를 정의하는 부분이다.
1) 단방향 데이터바인딩에 사용하는 바인딩 어댑터이다.
- 속성의 이름은 "content"라고 했고 MainViewModel의 MutableLiveData가 변경되면 CustomEditText의 값도 변경되게하는 바인딩 어댑터이다.
2) 이전 값과 비교하여 값이 달라졌을 때만 바인딩 어댑터가 작동하게한다.
- 이렇게 하지않으면 무한루프가 돈다;;
3) CustomEditText에 양방향 데이터 바인딩을 위한 리스너를 등록하는 바인딩 어댑터이다.
4) 이번 예제에서 양방향 데이터 바인딩에 가장 관련있는 뷰가 EditText이기 때문에 TextWatcher를 정의해준다.
5) afterTextChanged()에 InverseBindingListener.onChange()를 호출한다.
- 이렇게해야 언제 양방향 데이터바인딩일 발생할지 알 수가 있다.
6) 정의한 TextWatcher를 CustomEditText에 있는 EditText에 등록해준다.
7) 양방향 데이터바인딩에 사용되는 바인딩 어댑터이다.
- View에 변경된 값이 MainViewModel의 MutableLiveData로 전달이 되기 때문에 InverseBindingAdapter라고 한다.
- event 매개변수를 입력하여 언제 InverBindingAdapter가 작동하는지 알려준다.
처음보면 좀 어려울 수 있는데 데이터바인딩의 개념에 대해 잘 이해하고있으면 부드럽게 넘어가는 개념이다.
나도 여러 블로그를 참고하면서 이 예제를 작성만해봤지 실무에서 사용해본 적은 없다.
다음에 어쩔 수 없이 커스텀뷰를 만들어야할 때는 꼭 사용해봐야겠다.