[Android] DataBinding의 동작방식 - 6. InverseBinding (InverseBindingAdapter) + Two way Binding

그동안은 단방향 바인딩(One-way Binding)에 대해서만 알아봤다. 즉, 내가 어떤 Model을 XML로 넣어주면(setModel()) XML상에 정의한 규칙에 따라, Model의 정보들을 View에 넣어주는 것이었다. 하지만 반대로 View의 정보를 얻고 싶은 경우가 분명 생긴다. 그것도 View의 값이 변경되는 시점에 말이다.
 어쩌면 아직 감이 오지 않을수도 있고, 여기까지 왔다는 것은 InverseBinding의 존재를 알고 검색하다가 온 것일 수도 있다. 어찌되었건 InverseBinding은 생각보다 간단하고 더불어 Two-Way Binding까지 함께 살펴보자.


"[Android] DataBinding의 동작방식" 전체목록
   1. Setter Method와의 연결
   2. BindingAdapter의 기본 및 사용 시점
   3. BindingAdapter의 사용시 팁
   4. include Tag 혹은 ViewStub 사용시의 Binding
   5. Listener, Callback
   6. InverseBinding (InverseBindingAdapter) + Two way Binding

1. InverseBinding?

 Data Setting 관점에서 Binding이 Model To View 였다면, InverseBinding은 View To Model이다. InverseBinding은 사실 이것이 전부이다.
 언제 쓰는지 궁금할 수도 있다. 하나의 예를 들어보면 ViewPager와 Page Indicator의 관계를 들 수 있다. 아래의 그림을 보자.


위의 화면은 5개의 Page를 갖는 ViewPager와 현재 Page가 어디인지를 알려주는 Page Indicator로 구성되어있다. 그리고 이 Page Indicator는 ViewPager로부터 현재 페이지가 어디인지 받을 수 있어야 하며, 그를 위해 ViewPager에 OnPageChangeListener를 등록해서 처리할 수 있다. Flow를 보면 아래 그림과 같다.



 하지만 이젠 이것도 귀찮다는거다. 그냥 ViewPager의 Page가 변경될 때, XML 안에서 값을 받아서 Listener 등록 없이 Page Indicator가 받을 수 있으면 얼마나 좋을까? 즉, 아래와 같은 구조가 되는 것이다.



 ViewPager는 InverseBinding으로 ViewPager의 Page 변경 값을CurrentItem이라는 Model에 setting 하고, 그 값을 Binding으로 Page Indicator가 받아서 쓸 수도 있는 것이다.

※ 사실 위의 예시는 InverseBinding으로 얻은 결과를 Observable객체에 setting하고 이 Observable 객체에 Binding해둔 Page Indicator에 notify 되어 setting되는 구조이므로 단순한 InverseBinding은 아니긴 하다. 하지만 실 예시를 보여주기 위해 세부구조까지 설명한 것이니 이 시점에서 이해가 되지 않아도 된다. 차차 설명하도록 한다. 


2. InverseBinding의 구현

 InverseBindingAdapter를 직접 구현하는것 이전에, 이미 구현이 되어있는 EditText를 가지고 예를 들어보려고 한다. (정확히는 TextView 대상으로 InverseBindingAdapter가 정의되어 있다)
 InverseBinding을 하기 위해서는 아래와 같이 아주 약간의 문법 변화만 주면 된다.

- User.java
public class User {
    public ObservableField<String> firstName = new ObservableField<>();
    public ObservableField<String> lastName = new ObservableField<>();
    public User(String firstName, String lastName) {
        this.firstName.set(firstName);
        this.lastName.set(lastName);
    }
}

- Layout XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="user"
            type="com.example.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={user.firstName}" />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={user.lastName}" />
    </LinearLayout>
</layout>

일반 Binding과의 문법차이는 "@{...}" 와 "@={...}"이다. 등호 하나 차이이다.  등호가 들어가면 Two-Way, InverseBinding도 하겠다는 이야기이다. 주의할 점은 InverseBinding만 할 수 있는 것이 아니라, Binding, InverseBinding 둘다 적용된다는 점이다. (이것에 대해서는 추후 이야기 한다)
 이렇게 XML에 정의를 하면, EditText에서 Text 변경이 발생하면 user.firstName이나 user.lastName에 값이 Setting된다. 자, 이젠 위의 XML이 빌드되어 생성된 Binding Code를 살펴보자.
private android.databinding.InverseBindingListener mboundView1androidTextAttrChanged = new android.databinding.InverseBindingListener() {
    @Override
    public void onChange() {
        // Inverse of user.firstName.get()
        //         is user.firstName.set((java.lang.String) callbackArg_0)
        java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
        if (mUser != null && mUser.firstName != null) {
            mUser.firstName.set(((java.lang.String) (callbackArg_0)));
        }
    }
};

여기도 쓸데없는 코드들이 많아서 읽기 쉽게 정리를 한 것이다.
위의 코드에서 주요 세 부분에 대해서 Bold처리 해두었는데 정리하면

 - InverseBindingListener
 - TextViewBindingAdapter.getTextString() [@InverseBindingAdapter]
 - firstName.set()

 딱 여기까지만 보고 감이 왔으면 좋겠다. 위의 코드를 서술형으로 써보면
 1) InverseBindingListener를 등록해두면, 데이터 변경시 onChanged()가 호출된다. (값까지 받진 않는다)
 2) 이때 TextViewBindingAdapter.getTextString()을 통해 변경된 String Value를 가져오고.
 3) 그 값을 firstName에 setting해준다. 

복잡할 부분이 없는만큼 로직은 참으로 심플하다. 여기서 중요한 것은 InverseBindingListener의 onChanged()가 어디에선가 호출되는 것, 그리고 변경된 값을 적절히 받아올 수 있는 Getter Method가 InverseBindingAdapter가 정의되어 있는 것, 이라는 것을 알 수 있다.

위의 EditText의 경우는 이미 InverseBindingAdapter가 정의 되어있기 때문에 단순히 XML 작업만으로 손쉽게 되지만, 우리가 원하는 대부분의 경우는 우리가 직접 정의를 해야한다. 자 그럼 정의하러 가보자.


3. InverseBindingAdapter의 정의

 InverseBindingAdapter는 일반적인 BindingAdapter와는 Method부터가 다르다. 위의 예시에서 이미 확인 했지만, BindingAdapter가 Setter Method였다면, InverseBindingAdapter는 getter Method이다.
 우선 진행하기 전에, 위에서 봤던 TextViewBindingAdapter의 getTextString()부터 보고 넘어가자.

@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
    return view.getText().toString();
}

 InverseBindingAdapter를 정의하는 법은 BindingAdapter와 다르지 않고, Return값이 있는 getter라는 점도 앞에서 언급했다. 나머지 한가지 중요한 부분은 event Attribute이다. 이 부분을 처음 보는 사람들은 이런 생각일것같다 "이게 뭐야!!!$$^$^*##$@%!!!!" 필자가 그랬었으니...
 하지만, 간단한 부분이다. 이 Event는 위에서 언급한 InverseBindingListener의 onChanged()를 호출하기 위한 부분이다. 즉! 해당 값이 바뀌었음을 확인할 수 있어야 하는 부분이다. 그렇다더라도... 그걸 어떻게 확인하는가??? 자 아래부분을 보자. (실제 코드에서 이해를 돕기위해 중요부분만 잘랐다.)

@BindingAdapter("android:textAttrChanged")
public static void setTextWatcher(TextView view, final InverseBindingListener textAttrChanged) {
    view.addTextChangedListener(new TextWatcher() {
        ...
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            if (textAttrChanged != null) {
                textAttrChanged.onChange();
            }
        }
    });
}

앞에서 나온 event attribute의 이름 그대로 @BindingAdapter를 정의했고, 그 Parameter로 InverseBindingListener가 들어오는 것을 볼 수 있다. 우리는 이 Method 안에서 Text가 변경되었음을 확인해서 InverseBindingListener의 onChanged()를 호출만 해주면 된다!  TextView의 Text 변경여부 체크는 우리가 익히 알고 있는 TextWatcher를 사용하면 된다.

...?...?
의문이 드는 분들 있을것이다. 그렇다. 일반적으로 Code에서 우리가 하던 그 짓을 이렇게 별도 Method로 빼준것이다. 왜 이렇게 해야하는가?에 대한 대답을 한다면 필자는 세가지를 이야기 해줄 수 있을 것 같다.
  1) 이렇게 한번만 어딘가에 static 으로 정의를 해두면, 프로젝트 어디에서든 InverseBinding을 할 수 있다.
  2) MVVM을 적용할 때는, ViewModel에서 View 작업을 하지 않아야 하므로 이렇게 해둔다면 View 작업과의 분리가 가능하다.
  3) 결과적으로 ViewModel 코드들은 깔끔해진다. 


안타깝게도 우리가 InverseBinding을 사용하기 위해 정의해야할 것은 여기서 끝이 아니다.
필자가 앞서 이야기 했던 부분중... "@={}" 라는 문법은 InverseBinding만을 위한것이 아니라 Binding과 InverseBinding 둘다 하는 것이다! 라고 했던 부분이 있다. (기억이 안나는 사람들은 올라갔다 오기를...) 이게 무슨말이냐면... setter 역할을 하는 @BindingAdapter도 동일한 Attribute Name으로 정의해줘야 한다는 것이다.
즉! 단순히 EditText의 InverseBinding을 하기 위해선 아래 3가지 Method 세트를 구축해야한다.

@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
    ...
    view.setText(text);
}
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
    return view.getText().toString();
}
@BindingAdapter("android:textAttrChanged")
public static void setTextWatcher(TextView view, final InverseBindingListener textAttrChanged) {
    view.addTextChangedListener(new TextWatcher() {
        ...
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            if (textAttrChanged != null) {
                textAttrChanged.onChange();
            }
        }
    });
}

만약! InverseBinding으로만 쓰고 싶다면, 위의 Method 3개를 모두 정의는 한 다음에 setter 역할을 하는 @BindingAdapter("android:text") Method 내부를 비워주면 된다. Method 정의는 하고나서, 값을 setting하지 않으면 된다는 이야기이다.

바로 위의 Method 3종세트가 InverseBinding을 정의하고 싶을때 쓰는 방법이다.

4. Extra Tips!

여기서 몇가지 팁이 더 있다.

 1) @InverseBindingAdapter를 정의할 때 event를 설정해주지 않을 수도 있다. event를 설정해주지 않으면 attribute name에 "AttrChanged"라는 이름을 붙여서 자동으로 event가 정의된다.
  ex) Attribute Name = "testBinding"
       => event Name = "testBindingAttrChanged"

 2) @InverseBindingAdapter를 여러개 정의하다보면 Event를 처리하는 BindingAdapter가 중복되어 생성되는 경험을 하게 될 수 있다. 그럴때는, Event 처리용 BindingAdapter를 아래와 같이 정의할 수 있다.

@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
        "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, final TextViewBindingAdapter.BeforeTextChanged before,
                                  final TextViewBindingAdapter.OnTextChanged on, final TextViewBindingAdapter.AfterTextChanged after,
                                  final InverseBindingListener textAttrChanged) {
    ...
}

여기까지 Android의 DataBinding에 대해서 알아봤다. DataBinding에 대한 몇가지 팁이 더 있기도 하지만, 그건 시간이 허락한다면 한번 더 작성해보려고 한다.

댓글

이 블로그의 인기 게시물

[Android] DataBinding의 동작방식 - 4. include Tag 혹은 ViewStub 사용시의 Binding

[Android] DataBinding의 동작방식 - 5. Listener, Callback (CustomView의 Callback을 람다식으로 Binding하기)

[Android] DataBinding의 동작방식 - 2. BindingAdapter의 기본 및 사용 시점