Android

[Android] 안드로이드 양방향 데이터 바인딩 (2 way data binidng) 구현하기

NukeOlaf 2020. 8. 6. 08:30

Google I/O 2017 에서 Android Architecture Library 가 처음 발표되었다. AAC 의 가장 큰 핵심은 안드로이드의 컴포넌트들(주로 4대 컴포넌트라 일컬어지는 Activity, BroadcastReceiver, Service, ContentProvider)의 생명주기를 개발자가 좀더 다루기 쉽게 만들어주는 것에 있다.

AAC 라이브러리 구성요소 중에 데이터 바인딩 라이브러리(Data Binding Library)가 있다. 데이터 바인딩 라이브러리는 선언적 형식으로 UI 컴포넌트들과 데이터소스를 연결할 수 있는 라이브러리이다. (* 예전에 정리했던 데이터 바인딩 포스팅 : https://salix97.tistory.com/243) 이 데이터 바인딩 라이브러리의 바인딩 어댑터(Binding adapter) 를 통해 값을 set 해주는 프레임워크 호출을 관리할 수 있다. ex) setText() 또는 setOnClickListener() 등...

이번에는 단방향 데이터 바인딩이 아닌, 양방향 데이터 바인딩을 직접 커스텀하여 구현해보자. 양방향 데이터 바인딩이란, Model 에서 View 로, View 에서 Model 로 데이터의 변경이 통지되는 바인딩이다.

 

0. 양방향 데이터 바인딩을 통해 수행할 과제

현재 하고 있는 프로젝트에서 구현하려고 하는 기능이다. EditText 에 입력된 글자수에 따라서 글자의 크기가 변하는 기능을 구현하려고 한다. 글자는 최대 100자까지만 입력가능하다.

 

1. 레이아웃 작성하기

위의 툴바를 제외하고 레이아웃에서 EditText 부분만 추렸을때 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:bind="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="vm"
            type="com.az.create.CreateViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorDarkGrey"
        tools:context=".CreateFragment">

        <EditText
            android:id="@+id/humor_input"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:background="@drawable/bg_create_humor"
            android:gravity="center"
            android:maxLength="100"
            android:padding="30dp"
            android:textColor="@color/colorBlack"
            android:textSize="35sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            bind:flexibleSizeText="@={vm.humorText}" />

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

주목해서 보아야할 부분은 <EditText> 태그 안의 flexibleSizeText attribute 이다. flexibleSizeText 는 내가 직접 커스텀함 BindingAdapter 이다. 단방향 데이터 바인딩의 경우, "@{}" 표현식을 사용한다. 그러나, 위의 flexibleSizeText 와 같이 양방향 데이터 바인딩인 경우에는 "@={}" 표현식을 사용한다.

2. ViewModel 작성하기

class CreateViewModel : ViewModel() {

    val humorText = MutableLiveData<String>()

    init { humorText.value = "" }

}

ViewModel 에 humorText 라는 Observable 한 변수를 두었다. * 꼭 LiveData 가 아니더라도, ObservableField 자료형을 사용하여도 무방하다.

xml 의 EditText 와 VIewModel 의 humorText 변수를 바인딩해줄 것이다.

 

3. BindingAdapter 작성하기

@BindingAdapter("flexibleSizeText")
fun setFlexibleSizeText(view: EditText, text: String) {
    val length = text.length
    when {
        (length in 0..12) -> {
            view.setTextSize(Dimension.SP, 35F)
        }
        (length in 13..49) -> {
            view.setTextSize(Dimension.SP, 22F)
        }
        else -> {
            view.setTextSize(Dimension.SP, 16F)
        }
    }
}

@BindingAdapter("flexibleSizeTextAttrChanged")
fun setFlexibleSizeTextInverseBindingListener(view: EditText, listener: InverseBindingListener) {
    view.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            listener.onChange()
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {

        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        }
    })
}

@InverseBindingAdapter(attribute = "flexibleSizeText", event = "flexibleSizeTextAttrChanged")
fun getFlexibleSizeText(view: EditText): String {
    return view.text.toString()
}

양방향 데이터 바인딩을 직접 구현하기 위해서는 다음 세 개의 BindingAdapter 가 필요하다.

(1) 일반적인 단방향 데이터 바인딩에서도 사용하는 BindingAdapter

@BindingAdapter("flexibleSizeText")
fun setFlexibleSizeText(view: EditText, text: String) {
    val length = text.length
    when {
        (length in 0..12) -> {
            view.setTextSize(Dimension.SP, 35F)
        }
        (length in 13..49) -> {
            view.setTextSize(Dimension.SP, 22F)
        }
        else -> {
            view.setTextSize(Dimension.SP, 16F)
        }
    }
}

위의 코드에서 이 부분이다. text 의 길이에 따라 EditText 의 글자 크기를 변경해주는 코드가 포함되어 있다. 단방향 데이터 바인딩으로 구현하려고 하는 경우, 이 BindingAdpater 만 작성해주면 된다.

(2) InverseBindingAdapter 의 listener.onChange()  메서드를 호출하는데 사용하는 BindingAdapter

@BindingAdapter("flexibleSizeTextAttrChanged")
fun setFlexibleSizeTextInverseBindingListener(view: EditText, listener: InverseBindingListener) {
    view.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            listener.onChange()
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {

        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        }
    })
}

위에서 작성한 BindingAdapter 의 이름에 "AttrChanged" 를 붙여서 작명하는 것이 원칙이다. InverseBindingListener 의 onChange() 함수가 어디서 호출되는지를 정의해준다.

InverseBindingListener 는 xml 이 빌드될 때 생성되는 Binding Code 에서 등록되는데, view 의 데이터가 변경되었을 시 onChanged() 를 호출하는 리스너이다. 위의 코드에서는 TextWatcher 를 사용하여 Text 의 변경 여부를 확인하고, onChange() 를 호출하고 있다. 

(3) View 의 데이터를 ViewModel 에 설정해주는 InverseBindingAdpater 

@InverseBindingAdapter(attribute = "flexibleSizeText", event = "flexibleSizeTextAttrChanged")
fun getFlexibleSizeText(view: EditText): String {
    return view.text.toString()
}

마지막으로, getter 역할을 하는 InverseBindingAdapter 를 작성해준다. 맨 처음 작성했던 BindingAdpater 가 View 에 data 를 set 해주는 setter 역할을 한다면, InverseBindingAdpater 는 View 에서부터 data 를 가져오는 getter 역할을 한다. attribute 는 (1) 에서 만든 BindingAdapter 를, event 에는 (2) 에서 만든 BindingAdpater 를 달아준다. 그리고 View 로부터 가져온 data 를 반환하는 형식의 함수를 작성해주면 된다. event 는 data 가 바뀌어서 InverseBindingListener 의 onChange() 함수가 호출되는 부분을 의미한다.

InverseBindingAdpater 가 호출되면, View 에서 가져온 data 가 ViewModel 의 data 에 setting 된다.

 

4. Activity 코드 참고

class CreateActivity : AppCompatActivity() {

    private val viewModel: CreateViewModel by viewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil
            .setContentView<ActivityCreateBinding>(
                this,
                R.layout.activity_create
            ).apply {
                vm = viewModel
                lifecycleOwner = this
            }
    }

}

 

5. 완성된 모습

 

 

참고 사이트 >>>

http://blog.unsignedusb.com/2017/08/android-databinding-6-inversebinding.html

https://pyxispub.uzuki.live/?p=917