Android Data Binding의 DirtyFlag 알아보기

지난 Devfest Seoul 2018과 Devfest Busan 2018에서 ‘Android DataBinding for Modularization, ViewModel and Testing’이라는 주제로 발표를 했다.

데이터 바인딩의 꽃이라고 할 수 있는 양방향 데이터 바인딩, 이를 위한 DirtyFlag가 코드에서 어떻게 동작하는지 살펴보았다. 추가적으로 데이터 바인딩에 있는 Annotation의 사용법과 UI 모듈화, 그리고 Testing에 관해서 공유했다.

그냥.. 궁금했던 개발자 – Generated Binding Class

데이터 바인딩 라이브러리는 레이아웃(xml)의 변수(Variable)나 뷰(View)에 접근할 수 있는 바인딩 클래스이며 컴파일 타임에 생성된다. 예를 들어, layout 태그를 가지는 activity_main.xml 파일이 있다면 컴파일시 ActivityMainBinding 다음과 같은 Java 파일이 생성된다.

activity_main.xml

<layout …>
    <data>
        <import type="com.github.kimkevin.devfestseoul18.ValidationUtil"/>
        <variable
            name="vm"
            type="com.github.kimkevin.devfestseoul18.MainViewModel"/>
    </data>

    <android.support.constraint.ConstraintLayout …>
        <TextView
            android:text="@{vm.title}"…/>

        <EditText
            android:text="@={vm.authCode}"…/>

        <Button
            android:enabled="@{ValidationUtil.isCodeValid(vm.authCode)}"
            android:onClick="@{() -> vm.login()}"…/>
    </android.support.constraint.ConstraintLayout>
</layout>

위와 같은 xml이 Java 파일로 변경되었다면 레이아웃을 동작 시키기 위해 필요한 변수는 무엇일까?

  • 선언된 View에 접근하기 위한 ConstraintLayout, TextView, EditText, Button 4개의 변수
  • variable로 전달된 vm(ViewModel) 변수

그리고 추가적으로 특정 View와 이벤트를 전달하고 받을 수 있는 리스너가 필요하다.

  • View의 이벤트를 처리할 수 있는 리스너, Button의 onClickListener
  • 양방향 데이터 바인딩을 위한 역방향 바인딩 리스너, EditText에서 android:text에 사용되는 InverseBindingListener

실제로 생성된 ActivityMainBinding.class를 확인해보자.

public class ActivityMainTestBinding extends android.databinding.ViewDataBinding implements android.databinding.generated.callback.OnClickListener.Listener {
    …
    // views
    @NonNull public final android.widget.Button button;
    @NonNull public final android.widget.EditText editText;
    @NonNull private final android.support.constraint.ConstraintLayout mboundView0;
    @NonNull public final android.widget.TextView textView;
    // variables
    @Nullable private com.github.kimkevin.devfestseoul18.MainViewModel mVm;
    @Nullable private final android.view.View.OnClickListener mCallback1;
    // Inverse Binding Event Handlers
    private android.databinding.InverseBindingListener editTextandroidTextAttrChanged = new android.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
        …

TextView는 setText(vm.title) 호출로 데이터가 바인딩 되었을 것이고, EditText는 TextWatcher 등록으로 입력된 텍스트가 vm.authCode역바인딩 되었을 것이다. 그리고 Button에는 vm.login()호출되는 OnClickListener를 등록했을 것이다. 여기까지는 안드로이드 UI 개발을 해본 경험이 있다면 예측이 가능하다.

<Button android:enabled="@{ValidationUtil.isCodeValid(vm.code)}” />

만약 위의 코드가 동작하도록 실제 코드로 작성한다면 어떻게 만들 수 있을까? 실제로 개발하는 과정에서 있었던 나의 궁금증이었다. EditText의 텍스트가 변경될 때마다 button.setEnabled(ValidationUtil.isCodeValid(vm.authCode))을 리스너에서 호출해주면 될 것이라는 추측을 할 수 있다. 해답을 찾기 위해서 데이터 바인딩 라이브러리가 생성한 바인딩 클래스를 살펴보고 이를 주제로 발표하는 계기가 되었다.

/* flag mapping … */ 주석의 발견 – DirtyFlag

flag mapping

ActivityMainBinding.java의 하단에는 flag mppaing으로 시작하는 주석으로 16진수 flag와 데이터 바인딩에 사용되는 필드 정보(ex: vm)가 있다. 그럼 16진수의 flag는 어디에서 어떻게 사용되는 걸까? 단서를 찾아보자.

가장 먼저 flag 0은 0x1L(16진수)이며 vm.authCode와 관련 있는 코드를 찾아보았다.

private boolean onChangeVmAuthCode(android.arch.lifecycle.MutableLiveData<java.lang.String> VmAuthCode, int fieldId) {
    if (fieldId == BR._all) {
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        return true;
    }
    return false;
}

ViewModel의 authCode가 변경되면 mDirtyFlags|= 대입 연산자를 통해 vm.authCode(0x1L)와 OR 비트연산을 한다. 다음 0x2L 값을 가지는 flag 1의 vm과 관련 있는 코드를 찾아보았다.

public void setVm(@Nullable com.github.kimkevin.devfestseoul18.MainViewModel Vm) {
    this.mVm = Vm;
    synchronized(this) {
        mDirtyFlags |= 0x2L;
    }
    notifyPropertyChanged(BR.vm);
    super.requestRebind();
}

ViewModel이 바인딩 클래스에 setVm이 되면 위에서 처럼 mDirtyFlags는 |= 대입 연산자를 통해 vm(0x2L)과 OR 비트연산을 한다.

그러면 mDirtyFlags의 초기값을 무엇일까?

@Override
public void invalidateAll() {
    synchronized(this) {
            mDirtyFlags = 0x4L;
    }
    requestRebind();
}

ActivityMainBinding 바인딩 클래스의 생성자를 보면 가장 마지막에 invalidateAll() 함수가 호출된다. 되는데 이때 mDirtyFlags의 초기값이 설정된다.

DirtyFlag mapping

초기값이 0x4L인 이유는 플래그 비트가 flag << 1 하나씩 왼쪽으로 이동하기 때문이다. vm.authCode가 2진수 001(0x1L)이고 왼쪽으로 시프트 연산을 하면 vm은 2진수 010(0x2L)이다. 그럼 초기값 설정을 위해 플래그 비트를 하나 둔다면, 한번더 왼쪽으로 시프트 연산을 한 진수 100(0x4L)이 된다.

여기까지는 바인딩 클래스가 동작하기 위해서 필요한 플래그의 기본 정보에 대해서 살펴보았다.

DirtyFlags의 동작 살펴보기

지금까지 코드로 언급한 mDirtyFlags는 무엇일까? 처음 접하는 단어라 찾아보니 ZETAWIKI에서는 더티비트캐시 내용에 변경이 있었음을 기록한 플래그 비트이며 캐시 블록마다 더티 비트가 1개씩 있음. 0이면 ‘변동 없음’, 1이면 ‘변동 있음(갱신 필요[1])’의 의미 라고 말한다.

안드로이드의 데이터바인딩에서도 View(UI)에 데이터의 바인딩과 리바인딩을 위해 더티비트를 사용했다. 재밌는 비트라는 생각이 들었다.

그럼 데이터가 어떻게 바인딩 또는 리바인딩 되는지 직접 코드를 통해서 살펴보면서 앞서 가진 궁금증도 해결해보자.

View에 데이터가 바인딩되는 곳은 executeBindings함수에서 모두 호출된다.

@Override
protected void executeBindings() {
    ...
    dirtyFlags = mDirtyFlags;
    if ((dirtyFlags & 0x7L) != 0) {
        if ((dirtyFlags & 0x6L) != 0) {
             textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear);
        }
        // read vm.authCode.getValue()
        vmAuthCodeGetValue = vmAuthCode.getValue();
        // read ValidationUtil.isCodeValid(vm.authCode.getValue())
        vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength);
    }
    if ((dirtyFlags & 0x7L) != 0) {
        this.button.setEnabled(vmAuthCodeLengthInt6);
        TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue);
    }
    if ((dirtyFlags & 0x4L) != 0) {
        this.button.setOnClickListener(mCallback1);
        TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, 
                                              editTextandroidTextAttrChanged);
    }
    if ((dirtyFlags & 0x6L) != 0) {
        TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear);   
    }
}

코드를 보면 많은 mDirtyFlags의 조건문을 볼 수 있는데, Debug로 Breakpoint를 찍은 다음 플래그가 어떻게 변경되는지 살펴보자.

Debugging bbinding class by breakpoint - 1

바인딩 클래스를 생성하면 위에서 설명한 것처럼 mDirtyFlags0x4L로 초기화 된다.

Debugging bbinding class by breakpoint - 2

그리고 binding.setVm(viewModel)이 호출되면 mDirtyFlags는 flag vm(0x2L)와 mDirtyFlags |= 0x2L 비트 연산으로 0x06L이 된다. 바인딩 클래스가 생성되고 변수들이 셋되면 executeBindings()은 어떤 동작을 하게 될까?

Debugging bbinding class by breakpoint - 3

mDirtyFlags가 0x6L일 때, 동작하는 첫 번째 조건문의 0x6L & 0x7L 결과값은 0x6L로 0이 아니기 때문에 true가 된다. 마찬가지로 모든 조건문을 연산해보면 다음과 같은 결과를 확인할 수 있다.

Debugging bbinding class by breakpoint - 4

바인딩 클래스가 생성되고 변수들이 초기화 되면 executeBindings()에 있는 모든 조건문이 호출로 뷰에 데이터가 바인딩되고 리스너가 등록되는 것을 확인할 수 있다.

Debugging bbinding class by breakpoint - 5

그러면 글을 쓰도록 동기를 만들어 주었던 EditText의 텍스트 입력시 Button의 활성화는 어떻게 호출될까?

Debugging bbinding class by breakpoint - 6

먼저 ViewModel에 있는 데이터를 가져온 후에 this.button.setEnabled(vmAuthCodeLengthInt6)가 호출되는 것을 알 수 있다. executeBindings()에 많은 조건문이 있었던 이유는 데이터 변경시 필요한 로직 이외 다른 로직들은 변경 사항이 없기 때문에 호출되지 않도록 하기 위해서다. 이런 방식으로 데이터 바인딩 라이브러리는 데이터와 레이아웃을 바인딩하는데 필요한 글루 코드를 최소화할 수 있다.

평소에 개발관련 툴이나 라이브러리를 만드는 것에 관심이 많기 때문에 가끔씩 코드를 들여다 보면서 창작자의 철학이나 코드를 많이 배우고 있지만 이번 데이터 바인딩 라이브러리를 살펴보면서 더티 비트를 알게 되어 좋은 경험이 된 것 같다.