Observable Data Object Work

식별 가능성은 객체가 데이터 변경에 관해 다른 객체에 알릴 수 있는 기능을 의미한다. 데이터 바인딩 라이브러리를  통해 객체, 필드 또는 컬렉션을 식별 가능하게 만들 수 있다.

 

간단한 기존 객체를 데이터 바인딩에 사용할 수는 있지만 객체를 수정해도 UI가 자동으로 업데이트 되지는 않는다. 데이터 바인딩을 사용하면 데이터 변경 시 리스너라는 다른 객체에 알리는 기능을 데이터 객체에 제공할 수 있다.

식별 가능한 클래스에는 세 가지 유형, 즉 객체, 필드 및 컬레션이 있다.

식별 가능한 데이터 객체 중 하나가 UI에 결합되고 데이터 객체의 속성이 변경되면 UI가 자동으로 업데이트 된다.

 

식별 가능한 필드

일부 작업은 Observable 인터페이스를 구현하는 클래스를 생성하는 작업과 관련이 있지만 클래스에 몇 가지 속성만 있다면 그다지 애쓸 필요 없다. 이러한 상황에서는 일반 Observable 클래스 및 다음과 같은 Primitive 관련 클래스를 사용하여 필드를 식별 가능하게 만들 수 있다.

식별 가능한 필드는 단일 피드가 있는 독립적인 식별 가능한 객체이다. Primitive 버전은 엑세스 작업 중에 박싱 및 언박싱을 방지한다. 이 메커니즘을 사용하려면 다음과 같이 자바언어로는 public final, Kotlin으로는 읽기 전용 속성을 만들어야 한다.

    class User {
        val firstName = ObservableField<String>()
        val lastName = ObservableField<String>()
        val age = ObservableInt()
    }

    
    private static class User {
        public final ObservableField<String> firstName = new ObservableField<>();
        public final ObservableField<String> lastName = new ObservableField<>();
        public final ObservableInt age = new ObservableInt();
    }

    

 필드 값에 엑세스 하려면 다음과 같이 set() 및 get() 접근자 메소드를 사용하거나 Kotlin 속성 구문을 사용한다.

    user.firstName = "Google"
    val age = user.age

    

식별 가능한 컬렉션

일부 앱은 동적 구조를 사용하여 데이터를 보유한다. 식별 가능한 컬렉션을 통해 키를 사용하여 이러한 구조에 엑세스 할 수 있다.

Ex) 다음은 키가 String과 같은 참조 유형일 때는 ObservableArrayMap 클래스가 유용하다.

    ObservableArrayMap<String, Any>().apply {
        put("firstName", "Google")
        put("lastName", "Inc.")
        put("age", 17)
    }

    

다음 과 같이 레이아웃에서 문자열 키를 사용하여 맵을 찾을 수 있다.

<data>
        <import type="android.databinding.ObservableMap"/>
        <variable name="user" type="ObservableMap<String, Object>"/>
    </data>
    …
    <TextView
        android:text="@{user.lastName}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text="@{String.valueOf(1 + (Integer)user.age)}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

Ex) 다음과 같이 레이아웃에서 색인을 통해 목록에 엑세스 할 수 있다.

<data>
        <import type="android.databinding.ObservableList"/>
        <import type="com.example.my.app.Fields"/>
        <variable name="user" type="ObservableList<Object>"/>
    </data>
    …
    <TextView
        android:text='@{user[Fields.LAST_NAME]}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

식별 가능한 객체

Observable 인터페이스를 구현하는 클래스를 사용하면 식별 가능한 객체의 속성 변경에 관해 알림을 받으려는 리스너를 등록할 수 있다.

Observable 인터페이스에 리스너를 추가 및 삭제하는 매커니즘이 있지만 알림이 전송되는 시점은 개발자가 직접 결정해야 한다.

더 쉽게 개발할 수 있도록 데이터 바인딩 라이브러리는 리스너 등록 메커니즘을 구현하는 BaseObservable 클래스를 제공한다.

BaseObservable을 구현하는 데이터 클래스는 속성이 변경될 때 알리는 역할을 한다. 

Ex) 다음과 같이 Bindable 주석을 getter에 할당하고 setter의 notifyPropertyChanged()메소드를 호출함으로써 이 작업을 완료한다.

    class User : BaseObservable() {

        @get:Bindable
        var firstName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.firstName)
            }

        @get:Bindable
        var lastName: String = ""
            set(value) {
                field = value
                notifyPropertyChanged(BR.lastName)
            }
    }

데이터 바인딩은 데이터 바인딩에 사용된 리소스의 ID를 포함하는 모듈 패키지에 이름이 BR인 클래스를 생성한다. Binable 주석은 컴파일 중에 BR 클래스 파일에 항목을 생성한다. 데이터 클래스의 기본 클래스를 변경할 수 없으면 PropertyChangeRegistry 객체를 사용하여 Observable 인터페이스를 구현함으로써 효육적으로 리스너를 등록하고 리스너에 알림을 제공할 수 있다.

생성된 결합 클래스

데이터 바인딩 라이브러리는 레이아웃의 변수 및 뷰에 엑세스하는 데 사용되는 바인딩 클래스를 생성한다. 이번에는 생성된 바인딩 클래스를 만들고 커스텀하는 방법을 알아보자.

 

생성된 바인딩 클래스는 레이아웃 변수를 레이아웃 내의 뷰와 연결한다. 바인딩 클래스의 이름 및 패키지는 커스텀할 수 있다. 생성된 모든 바인딩 클래스는 ViewDataBinding 클래스에서 상속된다.

 

각 레이아웃 파일의 결합 클래스가 생성된다. 기본적으로 클래스 이름은 레이아웃 파일 이름을 기반으로 하여 파스칼 표기법으로 변환하고 Binding 접미사를 추가한다.

Ex) 상기 레이아웃 파일 이름은 activity_main.xml이므로 생성되는 클래스는 ActivityMainBinding이다. 이 클래스는 레이아웃 속성(Ex: user 변수)에서 레이아웃 뷰까지 모든 결합을 보유하며 결합 표현식에 값을 할당하는 방법도 인식한다.

 

결합 객체 만들기

레이아웃 내에서 표현식을 통해 뷰에 결합되기 전에는 뷰 계층 구조가 수정되지 않도록 하기 위해, 레이아웃 확장 후에 바로 결합 객체가 생성된다. 객체를 레이아웃에 결합하는 가장 일반적인 방법은 결합 클래스에서 정적 메소드를 사용하는 것이다.

Ex) 다음은 결합 클래스의 inflate() 메소드를 사용하여 뷰 계층 구조를 확장하고 객체를 뷰 계층 구조에 결합한다.

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

        val binding: MyLayoutBinding = MyLayoutBinding.inflate(layoutInflater)

        setContentView(binding.root)
    }

    

다음과 같이 LayoutInflater 객체 외에도 ViewGroup 객체를 사용하는 inflate() 메소드의 대체 버전이 있다.

    val binding: MyLayoutBinding = MyLayoutBinding.inflate(getLayoutInflater(), viewGroup, false)

    

레이아웃이 다른 매커니즘을 사용하여 확장되었다면 다음과 같이 별도로 결합될 수 있다.

    val binding: MyLayoutBinding = MyLayoutBinding.bind(viewRoot)

    

결합 유형을 미리 알 수 없는 상황도 있다. 그런 상황에서는 다음과 같이 DataBindingUtil 클래스를 사용하여 결합을 생성할 수 있다.

    val viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent)
    val binding: ViewDataBinding? = DataBindingUtil.bind(viewRoot)

    

Faagment, ListView 또는 RecyclerView Adapter에서 데이터 바인딩 항목을 사용하고 있다면, 다음 코드와 같이 바인딩 클래스 또는 DataBindingUtil 클래스의 inflate() 메소드를 사용할 수 있다.

    val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
    // or
    val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

    

ID가 있는 뷰

데이터 바인딩 라이브러리는 레이아웃에 ID가 있는 각 뷰의 결합 클래스에 불변 필드를 생성한다.

Ex) 데이터 바인딩 라이브러리는 다음 레이아웃에서 TextView 유형의 firstName 및 lastName 필드를 생성한다.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.firstName}"
       android:id="@+id/firstName"/>
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.lastName}"
      android:id="@+id/lastName"/>
       </LinearLayout>
    </layout>
    

라이브러리는 단일 패스로 뷰 계층 구조에서 ID가 포함된 뷰를 추출한다. 이 메커니즘은 레이아웃의 모든 뷰에 findViewById() 메소드를 호출하는 것보다 속도가 더 빠를 수 있다.

ID는 데이터 바인딩이 없을 때만큼 필요하지 않지만 코드에서 계속 뷰에 엑세스해야 하는 상황이 여전히 있다.

 

변수

데이터 바인딩 라이브러리는 레이아웃에 선언된 각 변수의 접근자 메소드를 생성한다.

Ex) 다음 레이아웃은 user, image, note 변수의 결합 클래스에 setter 및 getter 메서드를 생성한다.

<data>
       <import type="android.graphics.drawable.Drawable"/>
       <variable name="user" type="com.example.User"/>
       <variable name="image" type="Drawable"/>
       <variable name="note" type="String"/>
    </data>
    

ViewStub

일반 뷰와 달리 ViewStub 객체는 보이지 않는 뷰로 시작된다. 이 객체는 가시적으로 표시되거나 확장을 명시적으로 지시받으면 또 다른 레이아웃을 확장함으로써 레이아웃의 자체 뷰를 대체한다.

 

ViewStub이 기본적으로 뷰 계층 구조에서 사라지기 때문에 가비지 컬렉션을 통해 메모리 회수가 가능하도록 결합 객체의 뷰도 사라져야한다.

뷰가 최종적이므로 ViewStubProxy 객체는 생성된 결합 클래스에서 ViewStub를 대체하며, 이에 따라 개발자는 ViewStub가 존재할 경우 이에 엑세스 할 수 있고 ViewSub가 확장된 경우 확장된 뷰 계층 구조에도 엑세스 할 수 있다.

 

또 다른 레이아웃을 확장할 때 새 레이아웃의 결합을 설정해야 한다. 따라서 ViewStubProxy는 ViewStub OnInflateListener를 수신 대기하고 필요할 때 결합을 설정해야 한다. 지정된 시간에 하나의 리스너만 존재할 수 있으므로 ViewStubProxy를 통해 OnInflateListener를 설정할 수 있다. 설정된 이 리스너는 설정하면 호출된다.

즉시 결합

변수 또는 관찰 가능한 객체가 변경될  때 결합은 다음 프레임 이전에 변경되도록 예약된다. 하지만 결합이 즉시 실행되어야 하는 때도 있다. 이럴 때 강제로 실행하려면 executePendingBindings() 메소드를 사용하라.

 

고급 결합

동적 변수

구체적인 바인딩 클래스를 알 수 없을때도 있다.

Ex) 임의의 레이아웃에 작동하는 RecyclerView.Adapter는 특정 바인딩 클래스를 인식하지 못한다. 따라서 이 어댑터는 onBindViewHolder() 메소드를 호출하는 동안에도 계속해서 결합 값을 할당해야 한다.

Ex) 다음은 RecyclerView가 결합되는 모든 레이아웃에 item 변수가 있다.

BindingHolder 객체에는 ViewDataBinding 기본 클래스를 반환하는 getBinding() 메소드가 있다.

    override fun onBindViewHolder(holder: BindingHolder, position: Int) {
        item: T = items.get(position)
        holder.binding.setVariable(BR.item, item);
        holder.binding.executePendingBindings();
    }

    

* 데이터 바인딩 라이브러리는 모듈 패키지에 BR이라는 클래스를 생성한다. 이 클래스에는 데이터 바인딩에 사용된 리소스의 ID가 포함되어 있다. 위 예에서 라이브러리는 BR.item 변수를 자동으로 생성한다.

 

백그라운드 스레드

컬렉션이 아닌 한 백그라운드 스레드에서 데이터 모델을 변경할 수 있다. 데이터 바인딩은 계산 중에 각 변수/필드를 Localize하여 동시 실행 문제를 방지한다.

커스텀 바인딩 클래스 이름

기본적으로 바인딩 클래스는 레이아웃 파일 이름을 기반으로 하여 대문자로 시작하고 밑줄(_)을 삭제하며 다음 문자를 대문자로 표기하고 Binding이라는 단어를 접미사로 추가하는 방법으로 생성된다. 이 클래스는 모듈 패키지 아래의 databinding 패키지에 배치된다. 

Ex) 레이아웃 파일 contact_item.xml 이 ContactItemBinding클래스를 생성합니다. 모듈 패키지가 com.example.my.app 이면 바인딩 클래스가 com.example.my.app.databinding 패키지에 배치된다.

 

data요소의 class속성을 조정하여 바인딩 클래스의 이름을 바꾸거나 바인딩 클래스를 다른 패키지에 배치할 수 있다.

Ex) 다음 레이아웃은 현재 모듈의 databinding 패키지에 ContactItem 결합 클래스를 생성한다.

<data class="ContactItem">
        …
    </data>
    

클래스 이름 앞에 마침표를 접두사로 추가하여 다른 패키지에서 바인딩 클래스를 생성할 수 있다.

다음은 모듈 패키지에 바인딩 클래스를 생성한다.

<data class=".ContactItem">
        …
    </data>
    

또한 바인딩 클래스를 생성할 패키지의 전체 이름을 사용할 수 있다.

다음은 com.example 패키지에 ContactItem 결합 클래스를 만드는 예이다.

<data class="com.example.ContactItem">
        …
    </data>
    

+ Recent posts