본문 바로가기

[Android] DataBinding #4 - InverseBindingAdapter + Two-way Binding

DataBinding #4 - InverseBindingAdapter + Two-way Binding

 

 

DataBinding 이전 글

 

 

InverseBinding ?

  • 데이터 흐름의 방향을 의미

  Binding : Model To View (Model -> View)

  InverseBinding : View To Model (View -> Model) 의미지만 inverseBinding은 Binding의 역할도 포함

  Two-way Binding : Binding + InverseBinding의 의미로 사실 InverseBinding이 Two-way Binding이다

  • Binding과 InverseBinding의 문법차이는 '=' 등호 하나 차이

: Binding의 바인딩식 "@{...}" / InverseBinding의 바인딩식 "@={...}"

  '='등호가 포함되면 Two-way InverseBinding을 하겠다는 의미인데, InverseBinding + Binding이라 보면된다

 

 

 

InverseBinding 구현

  • Two-way Binding방식의 InverseBinding을 사용하는 방법엔 2가지가 존재합니다

1) 기본으로 정의된 InverseBindingAdapter 사용하는 방법

2) 사용자가 InverseBindingAdapter를 Custom하게 정의해서 사용하는 방법

대부분의 경우는 직접 InverseBindingAdapter를 정의해서 사용합니다

 

 


Basic InverseBindingAdapter 사용

- User.java (Model)

  • Java 코드
// JAVA
public class User extends BaseObservable {
    private String name;
    
    public User(String name) { this.name = name; }

    @Bindable		// onChange()에 반응할 Bindable 애노테이션 설정
    public String getName() {
        Log.d("테스트", "getName: "+ name);	// EditText 입력마다 name 값이 바뀌는지 로그 확인
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);     // "@{user.name}" -> getName() 메서드 사용하는 View에 변경알림
    }
}
  • Kotlin 코드
// Kotlin
class User(name: String) : BaseObservable() {
    @get:Bindable		// Getter 메서드에 @Bindable 주석을 의미
    var name: String = name
    	set(value) {		// Setter 메서드 재정의
        	field = value
        	notifyPropertyChanged(BR.name)
    	}
}



- activity_main.xml (Layout)

<?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">

    <data>
        <variable name="activity" type="com.jjj.databinding_sample.MainActivity" />
        <variable name="user" type="com.jjj.databinding_sample.User" />
    </data>

    <LinearLayout
        ... >
        
        <!-- "@={...}" - '='등호는 InverseBinding 의미 -->
        <EditText ...
            android:text="@={user.name}" />
        
        <!-- "@{...}" - 일반 Binding식 -->
        <TextView ...
            android:text="@{user.name}" />	
        
        <!-- 클릭 시 User객체 Log찍을 용도 -->
        <Button ...
            android:onClick="@{() -> activity.onClickCheck(user) }"/>
            
    </LinearLayout>
</layout>


: xml레이아웃을 보시면 <EditText>와<TextView>의 "android:text" 특성의 바인딩식을 보면 차이가 있습니다

바인딩식 "@{...}"은 '=' 등호를 사용하지 않으므로 일반 Binding을 의미하고 

바인딩식 "@={...}"은 '=' 등호를 사용하므로 InverseBinding을 의미합니다

위에서 데이터 흐름이 Binding은 "Model -> View", InverseBinding은 "View -> Model"이라고 하였는데

InverseBinding은 InverseBinding + Binding둘 다 한다는 점을 잊으면 안됩니다

쉽게 InverseBinding은 Getter / Binding은 Setter로 생각하고 진행 순서는 InverseBinding(Getter) 후 Binding(Setter)를 한다고 생각하면 이해하는데 조금 도움이 될 것 같습니다

 1) InverseBinding은 Getter로 EditText -> Model에 값을 입력

 2) Binding은 Setter로 입력된 Model 값을 -> EditText에 다시 세팅한다고 생각하면 됩니다

 

@NonNull
private final EditText mboundView1;	// xml레이아웃에 등록한 EditText 인스턴스 객체

private InverseBindingListener mboundView1androidTextAttrChanged = new InverseBindingListener() {
        @Override
        public void onChange() {			// InverseBindingListener 추상메서드 onChange() 구현
        
            // InverseBindingAdapter 메서드 getTextString()호출
            String callbackArg_0 = TextViewBindingAdapter.getTextString(mboundView1);
        	...

            userJavaLangObjectNull = (user) != (null);
            if (userJavaLangObjectNull) {
                // user.setName()
                user.setName(((String) (callbackArg_0)));
            }
        }
    };


: 안드로이드에서 기본적으로 지원하는 EidtText InverseBindingListener 구현부분입니다

- InverseBindingListener

- TextViewBindingAdapter.getTextString()  // @InverseBindingAdapter 메서드

- user.setName()

위 코드에서 중요한 3부분을 설명하면

1) InverseBindingListener를 등록하면, 해당 View 데이터 변경시 onChange()가 호출 (값 전달 X)

2) 이때 TextViewBindingAdapter.getTextString()을 통해 변경된 String Value를 가져오고

3) 그 값을 InverseBinding에 등록한 user.name에 Setting합니다

 

위 방법은 안드로이드에서 기본적으로 지원하는 InverseBindingAdapter를 사용한 방법으로

다른 View들도 존재하는데 필요에 따라 적절하게 사용하면 될것 같지만 대부분은 Custom해서 사용해야 할 것 같습니다

기본 지원 InverseBindingAdapter 종류

  • AbsListView android:selectedItemPosition
  • CalendarView android:date
  • CompoundButton android:checked
  • DatePicker android:year, android:month, android:day
  • NumberPicker android:value
  • RadioGroup android:checkedButton
  • RatingBar android:rating
  • SeekBar android:progress
  • TabHost android:currentTab
  • TextView android:text
  • TimePicker android:hour, android:minute

 

 


Custom InverseBindingAdapter 사용

InverseBindingAdapter 정의 (메서드)

: 기본 BindingAdapter와는 반환값(return)부터 다른 걸 알 수 있습니다.

앞서 말했듯이 BindingAdapter(Setter) / InverseBindingAdapter(Getter)라고 했듯이 아래 함수는 String을 반환

  • JAVA
// JAVA
@InverseBindingAdapter(attribute = "android:text", event = "textAttrChanged")
public static String getTextString(EditText view){
    return view.getText().toString();
}
  • Kotlin
// Kotlin
@JvmStatic
@InverseBindingAdapter(attribute = "android:text", event = "textAttrChanged")
fun String getTextString(view: EditText){
    return view.text.toString()
}

위 애노테이션을 보면 @InverseBindingAdapter(attribute = "android:text", event="textAttrChanged")

attribute는 BindingAdatper처럼 특성이름 의미, eventonChange()를 호출하기 위해 반응할 event를 의미

 

  • JAVA
// JAVA
@BindingAdapter("textAttrChanged")
public static void setTextWatcher(EditText view, final InverseBindingListener textAttrChagned){
    view.addTextChangedListener(new TextWatcher() {
        @Override
        public void afterTextChanged(@Nullable Editable p0) { }
        
        @Override
        public void beforeTextChanged(@Nullable CharSequence p0, int start, int before, int count) { }
        
        @Override
        public void onTextChanged(@Nullable CharSequence charSequence, int start, int before, int count) {
            if (textAttrChagned != null) {
                textAttrChagned.onChange();
            }
        }
    });
}
  • Kotlin
// Kotlin
@JvmStatic
@BindingAdapter("textAttrChanged")
fun setTextWatcher(view: EditText, textAttrChagned: InverseBindingListener){
    view.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(p0: Editable?) { }
        
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
        
        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            textAttrChagned?.onChange();
        }
    })
}


: @BindingAdapter의 attribute명을 위의 event의 이름 그대로 정의했고, 해당 BindingAdapter 메서드는
  파라미터로 InverseBindingListener가 들어오는 것을 볼 수 있습니다.

메서드 내부를 보면 EditText View에 TextChangedListener로 TextWatcher 리스너를 등록하였고, 이제부터 EditText의 값이 변경하면 TextWatcher로 인해 onTextChanged 메서드가 호출되고 InverseBindingListener를 통해 InverseBindingAdapteronChange()로 실행하게 됩니다  

여기서 어떻게 onChange()를 실행하면 여러 InverseBindingAdapter중에 내가 원하는걸 호출할 수 있지?
라는 의문이 생기는데,

정답은 InverseBindingListener가 BindingAdapter 특성InverseBindingAdapter의 event가 동일한 것을
찾아 호출(실행)하게 됩니다

 

여기서 모두 끝난게 아니라, "@={...}"는 말했듯이 InverseBindingAdapter + BindingAdapter의 기능을
모두 한다고 했습니다

그래서 Setter 역할인 @BindingAdapter동일한 AttributeName으로 정의해줘야 합니다

즉, 아래 3개의 메서드를 모두 선언해줘야 InverseBindingAdapter를 사용할 수 있습니다

  • JAVA
// BindingAdapter (Setter 역할)
@BindingAdapter("android:text")
public static void setTextString(EditText view, String content){
    String old = view.getText().toString();
    if (!old.equals(content)){
        view.setText(content);
    }
}

// InverseBindingAdapter (Getter 역할)
@InverseBindingAdapter(attribute = "android:text", event = "textAttrChanged")
public static String getTextString(EditText view){
    return view.getText().toString();
}
    
// InverseBindingListener (InverseBindingAdpater실행 역할)
@BindingAdapter("textAttrChanged")
public static void setTextWatcher(EditText view, final InverseBindingListener textAttrChagned){
    view.addTextChangedListener(new TextWatcher() {
        @Override
        public void afterTextChanged(@Nullable Editable p0) { }
        
        @Override
        public void beforeTextChanged(@Nullable CharSequence p0, int start, int before, int count) { }
        
        @Override
        public void onTextChanged(@Nullable CharSequence charSequence, int start, int before, int count) {
            if (textAttrChagned != null) {
                textAttrChagned.onChange();
            }
        }
    });
}
  • Kotlin
// BindingAdapter (Setter 역할)
@JvmStatic
@BindingAdapter("android:text")
fun getTextString(view: EditText, contet: String) {
    var old: String = view.text.toString()
    if (old != contet) view.setText(contet)
}

// InverseBindingAdapter (Getter 역할)
@JvmStatic
@InverseBindingAdapter(attribute = "android:text", event = "textAttrChanged")
fun getTextString(view: EditText): String? {
    return view.text.toString()
}
    
// InverseBindingListener (InverseBindingAdpater실행 역할)
@JvmStatic
@BindingAdapter("textAttrChanged")
fun setTextWatcher(view: EditText, textAttrChagned: InverseBindingListener ){
    view.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(p0: Editable?) { }
        
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }
        
        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            textAttrChagned?.onChange();
        }
    })
}

: @BindingAdapter(Setter)에서 주의할 점은 이전 값과 동일할 경우 setting을 하지 말아야 한다는 점입니다

setting시 EditText값 변경으로 인식해서 onChange() -> InverseBinding -> Binding 순서로 무한루프에 빠짐

그러므로 현재 EditText View의 값과 전달된 String값을 비교해서 동일한 경우에는 setting을 하지말아야 합니다

 

이벤트 Logic 발생 순서

  1. EditText 값 변경 시 InverseBindingListener.onChange() 호출
  2. 해당 event를 갖는 @InverseBindingAdapter (Getter) 메서드 실행
  3. @BindingAdapter (Setter) 메서드 실행

 

추가 Tips

  • @InverseBindingAdapter 정의 시 event 부분 생략 가능

: event를 생략 할 경우 attributeName + "AttrChanged"라는 이름의 event를 자동으로 등록해줍니다

// attributeName = "testBinding", event 생략
@InverseBindingAdapter(attribute = "testBinding")


// eventName = "testBindingAttrChanged" 자동으로 event 등록
@InverseBindingAdapter(attribute="testBinding", event="testBindingAttrChanged")

 

  • @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) {
            
            ... // Event 처리(Logic) 부분
}