Databinding - implement custom BindingAdapter

WindSekirun (wind.seo)·2022년 4월 26일
0

이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.

작성 시점: 2018-03-28

도입

최근 MVVM 을 도입하면서 Databinding 을 써야될 때가 왔는데, 정작 쓰려고 하니 좀 어려운 부분이 많았다.

이 글에서는 커스텀 뷰의 editText 에 onTextChanged 를 구현하는 작업을 설명하려 한다.

커스텀 뷰 구조

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <import type="android.view.View" />
        <variable
            name="inputView"
            type="..." />
    </data>
    <FrameLayout>
        <EditText
            android:id="@+id/editText"
            ... />
        <ImageView
            android:id="@+id/imgValid"
            ... />
    </FrameLayout>
</layout>

기본적인 EditText 에 regex 를 통한 valid 기능이 들어간 심플한 커스텀 뷰이다.

물론, 여기에 editText 의 hintText, inputType 등을 집어넣어서 바인딩 할 수도 있다.

문제

MVVM 의 규칙 중 프로세스 로직은 ViewModel 에 위임한다는 조건이 있어, XML 자체에 ViewModel 에 대한 의존성을 넣고 XML -> ViewModel 로 텍스트 변화나 버튼 클릭 등 이벤트를 Activity/Fragment 가 아닌 ViewModel 자체가 받게 했다.

보통, EditText 에 onTextChanged 를 붙이려면, 아래와 같이 구현하고

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

        <import type="android.view.View" />

        <variable
            name="viewModel"
            type="SignupHeightViewModel" />
    </data>

    <LinearLayout>

        ...

        <LinearLayout>

            <EditText
                android:id="@+id/editHeight"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_marginLeft="15dp"
                android:layout_weight="1"
                android:background="@drawable/transparent"
                android:gravity="right|center_vertical"
                android:inputType="number"
                android:onTextChanged="@{(text, start, before, count) -> viewModel.onHeightChanged(text)}"
                android:textColor="#000"
                android:textColorHint="#000000"
                android:textSize="15sp" />

            <TextView />
        </LinearLayout>

       ...
    </LinearLayout>
</layout>

ViewModel 에 메서드를 구현하면 된다.

public void onHeightChanged(CharSequence text) {
    if (!TextUtils.isEmpty(text)) {
        mHeight = Integer.parseInt(text.toString());
    } else {
        mHeight = 0;
    }
}

그런데, EditText 를 포함하는 커스텀 뷰에 onTextChanged 를 주면 찾을 수 없다며 오류가 난다.

이를 해결하는 방법은 DataBindingComponent 를 직접 구현하는 것이다.

DataBindingComponent 구현

public class BindingComponent implements DataBindingComponent {
    private final BindAdapter mAdapter;
    public BindingComponent(Context context) {
        this.mAdapter = new BindAdapter(context);
    }
    public static BindingComponent create(Context context) {
        return new BindingComponent(context);
    }
    @Override
    public BindAdapter getBindAdapter() {
        return mAdapter;
    }
}

사실상 공통 코드라 설명만 하자면, BindAdapter 라는 클래스를 DataBindingComponent 의 요소로서 사용할 수 있게 해주는 것이다.

데이터 바인딩 객체 생성이나 기본 컴포넌트 설정엔 이 클래스를 활용하고, 실제 기능을 붙이는 곳은 BindAdapter 인 셈이다.

기본 컴포넌트 설정엔 DataBindingUtil.setDefaultComponent(BindingComponent.create(this)); 을, 데이터 바인딩 객체 생성때는 DataBindingUtil.setContentView (Activity, layoutId, DataBindingComponent)를 사용한다.

BindAdapter 구현

기본 형태는 다음과 같다.

public class BindAdapter {
    final Context mContext;
    
    public BindAdapter(Context context) {
        mContext = context;
    }
}

데이터바인딩의 새 요소를 만들 때에는 @BindingAdapter("name") 가 달린 메서드를 구현하는데, 구조는 다음과 같다.

@BindingAdapter("onTextChanged")
public void bindOnTextChanged(InputView inputView, OnTextChanged on)

@BindingAdapter 에 들어가는 string 는 그 자체가 요소의 이름이 되어, app:onTextChanged 로 사용할 수 있게 해준다.

메서드의 첫번째 파라미터는 주입될 대상 뷰, 즉 여기서는 커스텀 뷰가 될 것이고, 후자에는 주입할 객체이다. 보통 String, int 등 자료형이 들어갈수도, 인터페이스 자체가 들어갈 수도 있다.

그러면 주입할 인터페이스를 정의한다.

public interface OnTextChanged {
        void onTextChanged(CharSequence s, int start, int before, int count);
}

원본 TextWatcher 가 제공하는 onTextChanged 그대로 사용했다.

이제 bindOnTextChanged 안을 구현하면 되는데, 아래의 순서를 거친다.

  1. TextWatcher 변수 구현 - onTextChanged 메서드에 on 파라미터에 대해 null-check 후 인터페이스 invoke
  2. ListenerUtil 로 기존에 등록된 TextWatcher 를 찾고, 있으면 제거
  3. 새로 만든 TextWatcher 부착

이를 코드로 구현하면 다음과 같다.

/**
 * binding 'onTextChanged' method with InputView
 * <p>
 * reference: https://android.googlesource.com/platform/frameworks/data-binding/+/android-6.0.0_r7/extensions/baseAdapters/src/main/java/android/databinding/adapters/TextViewBindingAdapter.java#299
 *
 * @param inputView
 * @param on
 */
@BindingAdapter("onTextChanged")
public void bindOnTextChanged(InputView inputView, OnTextChanged on) {
    final TextWatcher newValue = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            if (on != null) {
                on.onTextChanged(inputView.mBinding.editText.getText(), start, before, count);
            }
        }
        @Override
        public void afterTextChanged(Editable s) {
        }
    };

    final TextWatcher oldValue = ListenerUtil.trackListener(inputView.mBinding.editText, newValue, R.id.textWatcher);
    if (oldValue != null) {
        inputView.mBinding.editText.removeTextChangedListener(oldValue);
    }

    inputView.mBinding.editText.addTextChangedListener(newValue);
}

실제 사용

BindAdapter 를 만들고 DataBindingComponent 를 설정해주면 커스텀 요소를 쓸 수 있게 된다.

app:onTextChanged="@{(text, start, before, count) -> viewModel.onSchoolChanged(text)}"

참고로, viewModel::onSchoolChanged가 가장 깔끔하겠지만, 그렇게 되면 ViewModel 에 start, before, count 등의 쓰지 않는 파라미터를 구현해야 한다는 제약점으로 인해 XML 자체에서 파라미터를 선택해 반환해주는 형태를 취했다.

외전

@BindingAdapter("android:text")
public static void bindFloat(TextView view, float value) {
    view.setText(String.valueOf(value));
}

@BindingAdapter("android:text")
public static void bindDouble(TextView view, double value) {
    view.setText(String.valueOf(value));
}

@BindingAdapter("android:text")
public static void bindInt(TextView view, int value) {
    view.setText(String.valueOf(value));
}

@BindingAdapter("android:text")
public static void bindLong(TextView view, long value) {
    view.setText(String.valueOf(value));
}

@BindingAdapter("android:text")
public static void bindBoolean(TextView view, boolean value) {
    view.setText(String.valueOf(value));
}

int, double, float 등을 생각없이 넣었을 때에도 작동할 수 있게 보조해주는 @BindingAdapter 도 비슷하게 구현해보았다.

위의 app:onTextChanged 와 다르게 android 네임스페이스를 바로 작성했다는 것이 차이점이다.

profile
Android Developer @kakaobank

0개의 댓글