본문 바로가기
Android

[Android] Databinding의 원리에 대해 알아보자

by 촉수 2021. 6. 21.

안녕하세요. 안드로이드 개발할 때 데이터 바인딩을 많이 사용하고 계실 겁니다.

이 글은 데이터 바인딩의 사용법보다는 뷰에 데이터가 어떻게 맵핑되는지를 확인하려고 합니다.

글이 너무 길어서 읽기 싫으시면..

 

결론은 바인딩 클래스의 executeBindings()메서드에서 해당 뷰의 BindingAdapter 메서드를 호출해서 데이터를 바인딩한다 라고 볼 수 있습니다.

위의 결과는 TextView로 테스트했을 때 확인한 결과입니다.

 

시작할게요.

 

데이터바인딩을 사용하기 위해 제일 먼저 해야 하는 일은 build.gradle에 아래 설정을 추가하는 일입니다.

android {
    buildFeatures{
        dataBinding = true
    }
}

 

추가하면 어떤 일이 벌어질까요?

바로 External Libraries에 데이터바인딩 관련 라이브러리가 추가됩니다.

 

이제 activity_main.xml의 최상위 레이아웃을 layout태그로 감싸줍니다.

<layout>
    <androidx.constraintlayout.widget.ConstraintLayout 
        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"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

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

 

그러면 바인딩 클래스를 임포트할 수 있게 됩니다.

package com.kwancorp.databindingapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.kwancorp.databindingapp.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding // 바인딩 클래스

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

 

잠시만요! 위에 import문을 잠깐 보죠.

마지막 import문을 보면 프로젝트 패키지 아래 databinding.ActivityBinding을 임포트하고 있습니다.

하지만 프로젝트 구조에는 저런게 안보이죠..(

저게 정확히 뭔지 아시는 분은 댓글 좀..

)

그리고 ActivityBinding에 마우스를 갖다대보면 Lint에 아래와 같이 나옵니다.

 

그래서 저 final class의 선언부를 한번 보고 싶은데,

ActivityBinding에 ctrl + 마우스 왼쪽 클릭을 해보면 activity_main.xml로 갑니다..

그래서 일단 넘어갈게요.

 

이제 빌드를 한번 해봅니다.

빌드를 하면 아래와 같이 ActivityBindingImpl클래스가 생성됩니다.

 

자, 지금은 activity_main.xml에 ConstraintLayout밖에 없습니다.

activity_main.xml에 TextView를 2개 추가해 보겠습니다.

그러면 아래와 같이 ActivityBindingImpl 클래스가 변경됩니다. (빌드해야 바뀝니다)

public class ActivityMainBindingImpl extends ActivityMainBinding  {
    static {
        sIncludes = null;
        sViewsWithIds = new android.util.SparseIntArray();
        sViewsWithIds.put(R.id.textView, 1);
        sViewsWithIds.put(R.id.textView2, 2);
    }
    public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        this(bindingComponent, root, mapBindings(bindingComponent, root, 3, sIncludes, sViewsWithIds));
    }
    private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        super(bindingComponent, root, 0
            , (android.widget.TextView) bindings[1]
            , (android.widget.TextView) bindings[2]
            );
        ...
    }
}

 

새로운 View가 추가될 때마다 android.util.SparseIntArray에 View의 id가 추가되고 있네요.

그리고 ActivityMainBindingImpl의 생성자의 인자의 값이 바뀌고 있습니다.

이제 데이터가 어떻게 바인딩되는지 확인해 보겠습니다.

일단 ViewModel이랑 MainActivity를 연결해 주세요.

그리고 ViewModel에서 바인딩할 printMessage()메서드를 만듭니다.

// MainActivity
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
    }
}

// MainViewModel
class MainViewModel: ViewModel() {
    fun printMessage(): String {
        return "Hello World!"
    }
}

 

그리고 activity_main.xml에서 viewModel을 추가해줍니다.

<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="viewModel"
            type="com.kwancorp.databindingapp.MainViewModel" />
    </data>
    ...
</layout>

 

variable을 추가했더니 아래와 같이 변화하였습니다.

@SuppressWarnings("unchecked")
public class ActivityMainBindingImpl extends ActivityMainBinding  {
    ...
    @Override
    public void invalidateAll() {
        synchronized(this) {
                mDirtyFlags = 0x2L; // 0x1L -> 0x2L로 변경
        }
        requestRebind();
    }
    ...
    @Override
    public boolean setVariable(int variableId, @Nullable Object variable)  {
        boolean variableSet = true;
        // 이 부분 추가됨
        if (BR.viewModel == variableId) {
            setViewModel((com.kwancorp.databindingapp.MainViewModel) variable);
        } else {
            variableSet = false;
        }
        return variableSet;
    }

    // setViewModel메서드 추가됨
    public void setViewModel(@Nullable com.kwancorp.databindingapp.MainViewModel ViewModel) {
        this.mViewModel = ViewModel;
    }
}

 

setViewModel()메서드가 추가되었기 때문에 이제 Activity의 onCreate()에서 viewModel을 셋팅할 수 있습니다.

Kotlin으로 작성했기 때문에 setter메서드는 아래와 같이 사용합니다.

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    binding.viewModel = viewModel
}

 

이제 activity_main.xml에서 TextView에 viewModel.printMessage()를 바인딩합니다.

<TextView
    android:id="@+id/textView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@{viewModel.printMessage()}"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

 

실행해보면 화면에서 Hello World! 가 잘 나오는 것을 볼 수 있습니다.

잘되긴 하는데 맵핑이 과연 어떻게 된걸까요?

ActivityMainBindingImpl을 확인해보면 아래와 같습니다.

executeBindings() 메서드와 setViewModel메서드가 바뀐 것을 확인할 수 있을 겁니다.

public class ActivityMainBindingImpl extends ActivityMainBinding  {
    private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        ...
        this.textView.setTag(null);
        ...
    }

    public void setViewModel(@Nullable com.kwancorp.databindingapp.MainViewModel ViewModel) {
        this.mViewModel = ViewModel;

        // 이 부분이 추가됨
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        notifyPropertyChanged(BR.viewModel);
        super.requestRebind();
    }

    @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }

        // 이 부분이 추가됨
        java.lang.String viewModelPrintMessage = null;
        com.kwancorp.databindingapp.MainViewModel viewModel = mViewModel;
        if ((dirtyFlags & 0x3L) != 0) {
            if (viewModel != null) {
                // read viewModel.printMessage()
                viewModelPrintMessage = viewModel.printMessage();
            }
        }
        // batch finished
        if ((dirtyFlags & 0x3L) != 0) {
            // api target 1
            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.textView, viewModelPrintMessage);
        }
    }
}

 

데이터 바인딩시 플래그값으로 뭔가를 관리하고 있는 것 같습니다.

그리고 TextView에 데이터 셋팅은

executeBindings()메서드에서 TextViewBindingAdapter의 setText()메서드를 불러서 셋팅하고 있네요!

 

결론은

텍스트뷰에 데이터 바인딩시,

바인딩 클래스의 executeBindings()메서드 안에서 TextView의 BindingAdapter 중 setText()메서드를 호출해서

데이터를 할당하는 것이라고 볼 수 있습니다.

 

혹시, 이게 아니라면 댓글로 알려주시면 수정하도록 하겠습니다.

감사합니다.

'Android' 카테고리의 다른 글

Jitpack을 이용해서 Android OpenSource를 만들어 보자  (0) 2021.06.04

댓글