DataBinding 은 Data 와 Binding 이 합쳐진 단어로 직관적으로 해석하면 “데이터를 어딘가에 묶는 것” 입니다. 좀 더 자세히 말하면 데이터를 특정 UI Component 와 묶어서 데이터가 변경되면 UI Component 가 변경된 데이터를 반영할 수 있도록 하는 기술 정도로 이해할 수 있을 것 같습니다. 물론 two-way DataBinding 의 경우는 반대의 경로로도 갈 수 있습니다. Android 에서는 DataBinding 을 쉽게 구현할 수 있도록 Jetpack DataBinding Library 를 지원합니다.
이전까지는 DataBinding 이 무엇인지에 집중했습니다. 그렇다면 DataBinding 은 왜 필요할까요? Android 개발 관점에서 바라본다면 우선 DataBinding 을 사용하면 Activity 또는 Fragment 내의 UI 업데이트 로직을 Layout 으로 옮길 수 있게되며 이는 곧 관심사를 분리시켜 유지보수에 도움을 줄 수 있으며, 그리고 개인적으로 생각하는 가장 큰 필요성은 앱 내에 MVVM 패턴을 적용하여 UI 업데이트 로직을 신경쓰지 않고 앱의 비즈니스 로직에 집중하여 개발할 수 있다는 것입니다.
분석에 사용된 소스코드와 레이아웃은 아래와 같습니다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel by lazy { ViewModelProvider(this)[MainViewModel::class.java] }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.lifecycleOwner = this
binding.vm = viewModel
binding.button.setOnClickListener {
viewModel.update()
}
}
}
class MainViewModel : ViewModel() {
private val _count = MutableLiveData<Int>(0)
val count: LiveData<Int> get() = _count
fun update() {
_count.value = _count.value?.inc()
}
}
<?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="vm"
type="com.wantique.databinding.MainViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/address_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(vm.count)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<Button
android:id="@+id/button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
DataBindingUtil
클래스의 static 필드인 sMapper 에 DataBinderMapperImpl
인스턴스 참조를 저장합니다. DataBinderMapperImpl
클래스는 MergedDataBinderMapper
를 상속받으며 생성자에서 MergedDataBinderMapper
클래스의 addMapper()
메서드를 호출하여 빌드 시 생성된 DataBinderMapperImpl
인스턴스 참조를 List 에 저장합니다. DataBindingUtil.setContentView()
메서드의 마지막에 MergedDataBinderMapper
의 List 에 저장된 DataBinderMapperImpl
의 getDataBinder()
메서드를 호출하여 ViewDataBinding
을 상속받은 ActivityMainBindingImpl
인스턴스 참조를 반환합니다.public class DataBindingUtil {
private static DataBinderMapper sMapper = new DataBinderMapperImpl();
static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
int layoutId) {
return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
}
}
public class DataBinderMapperImpl extends MergedDataBinderMapper {
DataBinderMapperImpl() {
addMapper(new com.wantique.databinding.DataBinderMapperImpl());
}
}
public class MergedDataBinderMapper extends DataBinderMapper {
private List<DataBinderMapper> mMappers = new CopyOnWriteArrayList<>();
public void addMapper(DataBinderMapper mapper) {
Class<? extends DataBinderMapper> mapperClass = mapper.getClass();
if (mExistingMappers.add(mapperClass)) {
mMappers.add(mapper);
final List<DataBinderMapper> dependencies = mapper.collectDependencies();
for(DataBinderMapper dependency : dependencies) {
addMapper(dependency);
}
}
}
@Override
public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
int layoutId) {
for(DataBinderMapper mapper : mMappers) {
ViewDataBinding result = mapper.getDataBinder(bindingComponent, view, layoutId);
if (result != null) {
return result;
}
}
if (loadFeatures()) {
return getDataBinder(bindingComponent, view, layoutId);
}
return null;
}
}
public class DataBinderMapperImpl extends DataBinderMapper {
private static final int LAYOUT_ACTIVITYMAIN = 1;
private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(1);
static {
INTERNAL_LAYOUT_ID_LOOKUP.put(com.wantique.databinding.R.layout.activity_main, LAYOUT_ACTIVITYMAIN);
}
@Override
public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
if(localizedLayoutId > 0) {
final Object tag = view.getTag();
if(tag == null) {
throw new RuntimeException("view must have a tag");
}
switch(localizedLayoutId) {
case LAYOUT_ACTIVITYMAIN: {
if ("layout/activity_main_0".equals(tag)) {
return new ActivityMainBindingImpl(component, view);
}
throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
}
}
}
return null;
}
LifecycleOwner
의 타입이 Fragment
인 경우 viewLifecycleOwner
를 설정하라는 경로를 출력합니다. Fragment
를 LifecycleOwner
로 설정한 경우에는 Fragment
의 View
가 파괴된 경우에 LiveData
변경에 의한 View
업데이트가 발생한 경우 예외가 발생할 수 있습니다.LifecycleOwner
에 OnStartListener
Observer 를 등록하여 LifecycleOwner
의 Lifecycle 의 State 가 STARTED 로 이동할 때 DataBinding
클래스의 executePendingBindings()
메서드를 호출하여 데이터가 View 에 바인딩될 수 있도록 합니다.public abstract class ViewDataBinding extends BaseObservable implements ViewBinding {
@MainThread
public void setLifecycleOwner(@Nullable LifecycleOwner lifecycleOwner) {
if (lifecycleOwner instanceof Fragment) {
Log.w("DataBinding", "Setting the fragment as the LifecycleOwner might cause"
+ " memory leaks because views lives shorter than the Fragment. Consider"
+ " using Fragment's view lifecycle");
}
if (mLifecycleOwner == lifecycleOwner) {
return;
}
if (mLifecycleOwner != null) {
mLifecycleOwner.getLifecycle().removeObserver(mOnStartListener);
}
mLifecycleOwner = lifecycleOwner;
if (lifecycleOwner != null) {
if (mOnStartListener == null) {
mOnStartListener = new OnStartListener(this);
}
lifecycleOwner.getLifecycle().addObserver(mOnStartListener);
}
for (WeakListener<?> weakListener : mLocalFieldObservers) {
if (weakListener != null) {
weakListener.setLifecycleOwner(lifecycleOwner);
}
}
}
static class OnStartListener implements LifecycleObserver {
final WeakReference<ViewDataBinding> mBinding;
private OnStartListener(ViewDataBinding binding) {
mBinding = new WeakReference<>(binding);
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onStart() {
ViewDataBinding dataBinding = mBinding.get();
if (dataBinding != null) {
dataBinding.executePendingBindings();
}
}
}
}
ViewDataBinding
의 requestRebind()
메서드를 호출하여 LifecycleOwner
의 Lifecycle State 가 STARTED
이상인 경우 executePendingBindings()
메서드를 호출하여 데이터가 View 에 바인딩될 수 있도록 합니다.public class ActivityMainBindingImpl extends ActivityMainBinding {
public void setVm(@Nullable com.wantique.databinding.MainViewModel Vm) {
this.mVm = Vm;
synchronized(this) {
mDirtyFlags |= 0x2L;
}
notifyPropertyChanged(BR.vm);
super.requestRebind();
}
}
executeBindingsInternal()
메서드 내에서 ActivityMainBindingImpl
의 exectueBindings()
메서드를 호출합니다.executeBindings()
메서드 내에서 dirtyFlag
를 연산하여 연산 결과가 0이 아닌 경우 ViewModel 의 LiveData 를 가져온 후 updateLiveDataRegistration()
메서드를 호출하여 LiveData 에 Observer 를 등록합니다. 그리고 LiveData 에서 데이터를 가져온 후 DataBinding Library 에서 제공하는 기본 BindingAdapter 를 사용하여 데이터를 View 에 바인딩합니다.onChanged()
메서드가 호출되며, 최종적으로 exectueBindings()
메서드가 다시 호출되어 위와 동일한 과정을 거쳐 업데이트된 데이터를 View 에 바인딩합니다.public abstract class ViewDataBinding extends BaseObservable implements ViewBinding {
public void executePendingBindings() {
if (mContainingBinding == null) {
executeBindingsInternal();
} else {
mContainingBinding.executePendingBindings();
}
}
private void executeBindingsInternal() {
if (mIsExecutingPendingBindings) {
requestRebind();
return;
}
if (!hasPendingBindings()) {
return;
}
mIsExecutingPendingBindings = true;
mRebindHalted = false;
if (mRebindCallbacks != null) {
mRebindCallbacks.notifyCallbacks(this, REBIND, null);
// The onRebindListeners will change mPendingHalted
if (mRebindHalted) {
mRebindCallbacks.notifyCallbacks(this, HALTED, null);
}
}
if (!mRebindHalted) {
executeBindings();
if (mRebindCallbacks != null) {
mRebindCallbacks.notifyCallbacks(this, REBOUND, null);
}
}
mIsExecutingPendingBindings = false;
}
protected boolean updateLiveDataRegistration(int localFieldId, LiveData<?> observable) {
mInLiveDataRegisterObserver = true;
try {
return updateRegistration(localFieldId, observable, CREATE_LIVE_DATA_LISTENER);
} finally {
mInLiveDataRegisterObserver = false;
}
}
protected boolean updateRegistration(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return unregisterFrom(localFieldId);
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
registerTo(localFieldId, observable, listenerCreator);
return true;
}
if (listener.getTarget() == observable) {
return false;//nothing to do, same object
}
unregisterFrom(localFieldId);
registerTo(localFieldId, observable, listenerCreator);
return true;
}
protected void registerTo(int localFieldId, Object observable,
CreateWeakListener listenerCreator) {
if (observable == null) {
return;
}
WeakListener listener = mLocalFieldObservers[localFieldId];
if (listener == null) {
listener = listenerCreator.create(this, localFieldId, sReferenceQueue);
mLocalFieldObservers[localFieldId] = listener;
if (mLifecycleOwner != null) {
listener.setLifecycleOwner(mLifecycleOwner);
}
}
listener.setTarget(observable);
}
private static final CreateWeakListener CREATE_LIVE_DATA_LISTENER = new CreateWeakListener() {
@Override
public WeakListener create(
ViewDataBinding viewDataBinding,
int localFieldId,
ReferenceQueue<ViewDataBinding> referenceQueue
) {
return new LiveDataListener(viewDataBinding, localFieldId, referenceQueue)
.getListener();
}
};
private static class LiveDataListener implements Observer,
ObservableReference<LiveData<?>> {
final WeakListener<LiveData<?>> mListener;
public LiveDataListener(
ViewDataBinding binder,
int localFieldId,
ReferenceQueue<ViewDataBinding> referenceQueue
) {
mListener = new WeakListener(binder, localFieldId, this, referenceQueue);
}
@Override
public void addListener(LiveData<?> target) {
LifecycleOwner lifecycleOwner = getLifecycleOwner();
if (lifecycleOwner != null) {
target.observe(lifecycleOwner, this);
}
}
@Override
public void onChanged(@Nullable Object o) {
ViewDataBinding binder = mListener.getBinder();
if (binder != null) {
binder.handleFieldChange(mListener.mLocalFieldId, mListener.getTarget(), 0);
}
}
}
}
public class ActivityMainBindingImpl extends ActivityMainBinding {
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String stringValueOfVmCount = null;
com.wantique.databinding.MainViewModel vm = mVm;
androidx.lifecycle.LiveData<java.lang.Integer> vmCount = null;
int androidxDatabindingViewDataBindingSafeUnboxVmCountGetValue = 0;
java.lang.Integer vmCountGetValue = null;
if ((dirtyFlags & 0x7L) != 0) {
if (vm != null) {
// read vm.count
vmCount = vm.getCount();
}
updateLiveDataRegistration(0, vmCount);
if (vmCount != null) {
// read vm.count.getValue()
vmCountGetValue = vmCount.getValue();
}
// read androidx.databinding.ViewDataBinding.safeUnbox(vm.count.getValue())
androidxDatabindingViewDataBindingSafeUnboxVmCountGetValue = androidx.databinding.ViewDataBinding.safeUnbox(vmCountGetValue);
// read String.valueOf(androidx.databinding.ViewDataBinding.safeUnbox(vm.count.getValue()))
stringValueOfVmCount = java.lang.String.valueOf(androidxDatabindingViewDataBindingSafeUnboxVmCountGetValue);
}
// batch finished
if ((dirtyFlags & 0x7L) != 0) {
// api target 1
androidx.databinding.adapters.TextViewBindingAdapter.setText(this.addressTextView, stringValueOfVmCount);
}
}
}
DataBinding 은 데이터가 업데이트되었을 때 UI Component 가 자동으로 업데이트될 수 있도록 지원합니다. Android 는 Jetpack 에서 DataBinding Library 를 제공하여 매우 간편하게 DataBinding 을 적용할 수 있습니다.