본문 바로가기

android/Jetpack

4-1 DataBinding

*개인적으로 분석한 내용이므로 틀린 내용이 있을 수 있습니다. 틀린 내용을 댓글로 남겨주시면 수정하도록 하겠습니다!

 

DataBinding 라이브러리는 UI Component 들이 데이터 소스에 Programmatically 한 방식이 아닌 선언적 방식을 통해 결합하도록 지원하는 Jetpack 라이브러리이다.

 

[1]. DataBinding 사용방법

 *DataBinding 을 사용하는 방법은 수많은 블로그에 이미 자세히 기록되어 있기 때문에 간략히 작성하고 넘어간다.

 

 1. gradle 파일에서 DataBinding 활성화

    buildFeatures {
        dataBinding true
    }

 

2. layout 파일을 DataBinding 사용 형식으로 변경

 <data> 내의 <variable> 태그를 선언하면 DataBinding 라이브러리에 의해 자동으로 생성되는 Binding class 의 멤버변수로 선언되게 된다.

<?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="testViewModel"
            type="com.test.databinding.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/output_text_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{testViewModel.count.toString()}"
            android:textSize="30sp"
            android:textColor="@color/black"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Button
            android:id="@+id/increase_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/output_text_view"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginTop="30dp"
            android:text="increase" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 

3. DataBinding 에 사용할 ViewModel class 생성

 해당 ViewModel 을 Binding class에서 참조하고 있으며, layout xml 파일의 View 의 속성이 ViewModel 의 LiveData를 참조하고 있기 때문에 LiveData 의 값이 업데이트되면 자동으로 View 도 업데이트 될 수 있다.

class MainViewModel : ViewModel() {
    private var _count: MutableLiveData<Int> = MutableLiveData()
    val count: LiveData<Int> get() = _count

    init {
        _count.value = 0
    }

    fun increase() {
        _count.value = _count.value?.plus(1)
    }
}

 

4. Activity Component 에서 DataBinding 초기화

class MainActivity : AppCompatActivity() {
    private val binding by lazy { DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)}
    private val viewModel by lazy { ViewModelProvider(this).get(MainViewModel::class.java) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        initDataBinding()
        initViews()
    }

    private fun initDataBinding() {
        binding.lifecycleOwner = this

        binding.testViewModel = viewModel
    }

    private fun initViews() {
        binding.increaseButton.setOnClickListener {
            viewModel.increase()
        }
    }
}

 

[2]. LiveData 옵저버를 등록하는 코드 분석

 DataBinding 을 LiveData 와 함께 사용하지 않으면 데이터가 변경되었을 때 invalidateAll 메서드를 호출하여 데이터가 변경되었음을 View 에 직접 알려야 한다. 하지만 LiveData 를 사용하면 데이터가 변했을 때 옵저버를 통해 자동으로 View 에 통지가 가기 때문에 더 간결한 코드를 작성할 수 있다. 이 섹션에서는 DataBinding  과 LiveData 를 함께 사용할 경우 어떻게 옵저버를 등록하게 되는지를 분석한다.

 

1. 생명주기 소유자의 생명주기가 ON_START 가 되면 리스너를 통해 LiveData 의 값을 View 에 최초로 바인딩하기 위한 작업을 시작한다.

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();
		}
	}
}

 

2. 데이터를 바인딩하기 전에 옵저버가 등록되어 있는 LiveData 인지 검사하는 메서드인 updateLiveDataRegistration 을 호출한다. 만약 옵저버가 등록되지 않은 LiveData 라면(최초의 바인딩이라면) 이 곳에서 옵저버를 등록한다.

@Override
protected void executeBindings() {
	long dirtyFlags = 0;
	synchronized(this) {
		dirtyFlags = mDirtyFlags;	//dirtyFlag == 0x6L
		mDirtyFlags = 0;
	}
	com.test.databinding.MainViewModel testViewModel = mTestViewModel;
	androidx.lifecycle.LiveData<java.lang.Integer> testViewModelCount = null;
	java.lang.Integer testViewModelCountGetValue = null;
	java.lang.String testViewModelCountToString = null;

	if ((dirtyFlags & 0x7L) != 0) {
		if (testViewModel != null) {
			// read testViewModel.count
			testViewModelCount = testViewModel.getCount();
		}
		
		//옵저버가 등록되지 않은 LiveData 라면 옵저버를 등록하는 메서드
		updateLiveDataRegistration(0, testViewModelCount);
		
		if (testViewModelCount != null) {
			// read testViewModel.count.getValue()
			testViewModelCountGetValue = testViewModelCount.getValue();
		}

		if (testViewModelCountGetValue != null) {
				testViewModelCountToString = testViewModelCountGetValue.toString();
		}
	}
	// batch finished
	if ((dirtyFlags & 0x7L) != 0) {
		// api target 1
		androidx.databinding.adapters.TextViewBindingAdapter.setText(this.outputTextView, testViewModelCountToString);
	}
}

 

protected boolean updateLiveDataRegistration(int localFieldId, LiveData<?> observable) {
	mInLiveDataRegisterObserver = true;
	try {
		return updateRegistration(localFieldId, observable, CREATE_LIVE_DATA_LISTENER);
	} finally {
		mInLiveDataRegisterObserver = false;
	}
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
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;
}

 

[3]. LiveData 의 값이 업데이트될 때 View 를 자동으로 업데이트하는 내용 분석

 1. 관찰대상인 LiveData 의 값이 변화하면 자동으로 onChanged 메서드가 호출된다.

@Override
public void onChanged(@Nullable Object o) {
	//binder == ActivityMainBindingImpl 객체
	ViewDataBinding binder = mListener.getBinder();
	if (binder != null) {
		//target == 관찰중인 LiveData 객체
		binder.handleFieldChange(mListener.mLocalFieldId, mListener.getTarget(), 0);
	}
}

 

 2. requestRebind 메서드를 호출하여 변화된 값에 대해 View 업데이트를 시작한다.

protected void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
	if (mInLiveDataRegisterObserver || mInStateFlowRegisterObserver) {
		// We're in LiveData or StateFlow registration, which always results in a field change
		// that we can ignore. The value will be read immediately after anyway, so
		// there is no need to be dirty.
		return;
	}
	boolean result = onFieldChange(mLocalFieldId, object, fieldId);
	if (result) {
		requestRebind();
	}
}
protected void requestRebind() {
	if (mContainingBinding != null) {
		mContainingBinding.requestRebind();
	} else {
		final LifecycleOwner owner = this.mLifecycleOwner;
		if (owner != null) {
			Lifecycle.State state = owner.getLifecycle().getCurrentState();
			if (!state.isAtLeast(Lifecycle.State.STARTED)) {
				return; // wait until lifecycle owner is started
			}
		}
		synchronized (this) {
			if (mPendingRebind) {
				return;
			}
			mPendingRebind = true;
		}
		if (USE_CHOREOGRAPHER) {
			mChoreographer.postFrameCallback(mFrameCallback);
		} else {
			mUIThreadHandler.post(mRebindRunnable);
		}
	}
}

private final Runnable mRebindRunnable = new Runnable() {
	@Override
	public void run() {
		synchronized (this) {
			mPendingRebind = false;
		}
		processReferenceQueue();
		if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
			// Nested so that we don't get a lint warning in IntelliJ
			if (!mRoot.isAttachedToWindow()) {
				// Don't execute the pending bindings until the View
				// is attached again.
				mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
				mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
				return;
			}
		}
		executePendingBindings();
	}
};

 

3. 최종적으로 Binding Impl 클래스의 executeBindings 메서드를 호출하여 View 업데이트하는 코드를 실행한다.

@Override
protected void executeBindings() {
	long dirtyFlags = 0;
	synchronized(this) {
		dirtyFlags = mDirtyFlags;	//dirtyFlag == 0x6L
		mDirtyFlags = 0;
	}
	com.test.databinding.MainViewModel testViewModel = mTestViewModel;
	androidx.lifecycle.LiveData<java.lang.Integer> testViewModelCount = null;
	java.lang.Integer testViewModelCountGetValue = null;
	java.lang.String testViewModelCountToString = null;

	if ((dirtyFlags & 0x7L) != 0) {
		if (testViewModel != null) {
			// read testViewModel.count
			testViewModelCount = testViewModel.getCount();
		}
		updateLiveDataRegistration(0, testViewModelCount);
		if (testViewModelCount != null) {
			// read testViewModel.count.getValue()
			testViewModelCountGetValue = testViewModelCount.getValue();
		}

		if (testViewModelCountGetValue != null) {
	
			//TextView 의 Text 를 업데이트한다.
			testViewModelCountToString = testViewModelCountGetValue.toString();
		}
	}
	// batch finished
	if ((dirtyFlags & 0x7L) != 0) {
		// api target 1
		androidx.databinding.adapters.TextViewBindingAdapter.setText(this.outputTextView, testViewModelCountToString);
	}
}

 

'android > Jetpack' 카테고리의 다른 글

5-1. Jetpack Navigation의 구성요소  (0) 2022.10.09
4-2 단방향 데이터 바인딩, 양방향 데이터 바인딩  (0) 2022.10.08
3. LiveData  (0) 2022.10.03
2. ViewModel 과 ViewModelFactory  (0) 2022.10.02
1. ViewBinding  (1) 2022.10.01