이 글은 기존 운영했던 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 를 직접 구현하는 것이다.
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)
를 사용한다.
기본 형태는 다음과 같다.
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 안을 구현하면 되는데, 아래의 순서를 거친다.
이를 코드로 구현하면 다음과 같다.
/**
* 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 네임스페이스를 바로 작성했다는 것이 차이점이다.