안드로이드/안드로이드 공부

[안드로이드/Kotlin] Binding(View, Data)

요빈 2023. 4. 12. 11:18

Binding

 

Gradle.build(module)

 

android 블록 내 다음 코드를 추가한다.

buildFeatures {
    dataBinding true
    viewBinding true
}

바인딩 옵션을 활성화시키므로써 레이아웃 파일에 바인딩 클래스가 생기고, 선언해두었던 뷰들의 id가 참조된다. 또한, 뷰를 데이터 개체와 결합하는 데 필요한 클래스도 자동으로 생성된다.

 


ViewBinding

뷰 바인딩을 적용하기 전에는 액티비티에서 텍스트뷰의 값을 변경하는 등의 UI 컴포넌트와 데이터 관련 작업을 하기 위해, findViewById를 이용해 xml의 뷰와 변수를 연결시켜줘야 했다.

앱을 개발하다보면 알다시피 하나의 레이아웃에는 수 많은 컴포넌트가 있기 때문에 코드가 항상 길어지고 지저분했다.

뷰 바인딩은 이런 불편함을 덜어주는 기능이다.

 

- 바인딩 변수 선언

 

  • 바인딩 클래스 명은 레이아웃 파일을 카멜 형식(단어 첫 글자 대문자)으로 바꾸고 뒤에 Binding이 붙는다.
    • 예를 들어, activity_main의 바인딩 클래스명은 ActivityMainBinding이다.
  • lateinit 또는 null 허용(?)값과 사용값 나눠 선언 -> 프래그먼트 인스턴스 생성 시점과 바인딩 클래스 사용 시점 사이에 텀이 있기 때문에 지연초기화 또는 null값으로 초기화해주는 것이 좋다.
// Activity
private lateinit var binding: ActivityMainBinding

// Fragment
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!

 

- 바인딩 이용해 inflate

 

  • Activity의 경우 inflate 시 layoutInflater 사용
  • Fragment의 경우 inflate 시 inflater 인수를 받아와 뷰와 연결
  • 바인딩된 뷰의 최상위 뷰(root) 인스턴스를 액티비티에 표시 
class MainActivity : AppCompatActivity() {

    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}
class MainFragment : Fragment() {

    private var _binding: FragmentMainBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        _binding = FragmentMainBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        _binding = null
        super.onDestroyView()
    }
}

* 프래그먼트에서는 액티비티와 달리 onDestroyView 메서드를 재정의해줘야 한다!  바인딩 클래스는 뷰에 대한 참조를 가지고 있기 때문에 뷰가 제거될 때(onDestroyView), 이 바인딩 클래스의 인스턴스도 함께 정리해줘야 한다.

 

+) 특정 레이아웃에서 바인딩 클래스가 필요없을 경우, viewBindingIgnore 속성을 레이아웃 루트뷰에 추가하면 바인딩 클래스가 생성되지 않는다.

<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"
    tools:viewBindingIgnore="true" // 뷰 바인딩 클래스 생성을 안하고 싶을 때
    tools:context=".HelloActivity">
 
</androidx.constraintlayout.widget.ConstraintLayout>

DataBinding

데이터 바인딩은 데이터와 뷰를 연결하는 작업을 레이아웃 파일에서 처리하는 기술을 말한다.

즉, 레이아웃(Activity, Fragment) 파일에서 데이터를 따로 지정해주지 않아도 레이아웃 파일에서 자동으로 데이터를 반영한다는 의미이다.

레이아웃 파일에서 구성요소를 결합(Binding)하면 자연스레 UI 클래스에는 로직만을 위한 코드만 남게되어 파일이 더욱 단순화되고, 유지관리 또한 쉬워진다. 결과적으로 앱 성능이 향상되며 메모리 누수 및 null 포인터 예외를 방지할 수 있다.

뷰와 관련된 작업은 레이아웃 파일에서 정의된다.

 

- DataBinding vs ViewBinding

데이터 바인딩과 뷰 바인딩을 다이어 그램으로 나타내면 다음과 같이 나타낼 수 있다.

 

즉, 데이터 바인딩은 뷰 바인딩의 역할도 할 수 있을뿐더러 추가적으로 동적 UI 컨텐츠 선언, 양방향 데이터 결합 등을 지원한다.

하지만 뷰 바인딩이 상대적으로 간단하며 퍼포먼스 효율이 좋고, 용량이 절약된다는 장점이 있다.

실제로 구글 공식문서에서는 단순히 findViewById를 대신 사용하는 거라면, 뷰 바인딩을 사용할 것을 권장하고 있다.

 

- DataBinding 사용

바인딩 객체 생성 방법은 viewBinding과 동일하다.

하지만 뷰 바인딩과 데이터 바인딩을 함께 사용해 결합 유형을 미리 알 수 없는  상황에서는 DataBindingUtil 클래스를 사용한다. 

그러면 binding.레이아웃에서 선언한 바인딩 변수 형식으로 접근이 가능하다.

* Activity 파일엔 바인딩 변수를 연결하는 예제들을 추가하였다.

* layout inflate -> bind -> setContentView

class MainActivity : AppCompatActivity() {
 
    private lateinit var binding: ActivityMainBinding
    private val exampleViewModel: ExampleViewModel by viewModels()
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        
        binding.user = User("지은", "이")
        binding.setActivity(this)
        binding.viewModel = exampleViewModel
        binding.lifecycleOwner = this
    }
}
// Fragment
class StartFragment : Fragment() {

    private var binding: FragmentStartBinding? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_start, container, false)

        return binding.root
    }
    
// ListView, RecyclerView
 val listItemBinding = ListItemBinding.inflate(layoutInflater, viewGroup, false)
// or
val listItemBinding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false)

* Fragment, Listview 또는 RecyclerView 어댑터 내에서 데이터 바인딩을 사용하면 위와 같이 사용 가능

 

- 레이아웃

데이터 바인딩을 사용하려면 xml 파일의 가장 바깥을 <layout> 태그로 감싸줘야 한다.

그래야 데이터 결합에 필요한 클래스들을 사용 및 적용할 수 있다.

<?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="user"
            type="com.example.myApplication.data.User" />
        <variable
            name="activity"
            type="com.example.myApplication.MainActivity"/>
    </data>
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
 
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{activity::onTextClick}"
            android:text="@{user.firstName}" />
 
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{user.lastName}" />
    </LinearLayout>
</layout>

 

layout 태그와 뷰 컴포넌트 사이에 <data> 태그, <variable> 태그를 사용해 바인딩할 변수를 선언한다.

type에 연결하고 싶은 데이터가 있는 경로를 적어주고, name에 변수명을 정해주면 된다.

바인딩 데이터는 '@{}' 안에 넣어 사용할 수 있다.

표현식에는 다음과 같은 기능들을 사용할 수 있다.

 

  • 기본적인 연산(산술, 논리, 바이너리, 메서드 호출)
  • 속성 참조 (ex. @{user.lastName})
  • null 예외 방지 (ex. 위의 예시에서 user가 null이면, user.lastName에 null이 기본값으로 할당)
  • 뷰 참조: id로 레이아웃의 다른 뷰 참조 가능
  • 리소스 (ex. "@{@string/name}")
  • 이벤트 처리 (ex. onclick(), onZoomIn())
  • 메서드 참조(데이터 바인딩 시 생성) / 리스너 결합(이벤트 발생 시 실행) (ex. @{() ->  ~ }
  • import, variable, include(다른 레이아웃 파일에 결합된 데이터 사용 가능)

- Observable한 데이터 객체로 작업

뷰에 즉각적으로 보여지기 위해 바인딩할 변수를 Observable한 타입으로 선언해야 한다.

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

대표적으로 ObservableField와 LiveData가 있다.

 

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

* 데이터 바인딩은 ViewModel과 많이 사용되고 있다!

- BindingAdapter

바인딩 어댑터는 적절한 프레임워크를 호출해 값을 설정하는 작업을 한다.

값을 설정하기 위해 호출되는 메서드를 지정하고, 고유한 바인딩 로직을 제공하는 어댑터를 사용해 반환 객체의 유형을 지정할 수 있다.

   @BindingAdapter("app:goneUnless")
    fun goneUnless(view: View, visible: Boolean) {
        view.visibility = if (visible) View.VISIBLE else View.GONE
    }
    
     @BindingAdapter("imageUrl", "error")
    fun loadImage(view: ImageView, url: String, error: Drawable) {
        Picasso.get().load(url).error(error).into(view)
    }
    
    @BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
    fun setImageUrl(imageView: ImageView, url: String?, placeHolder: Drawable?) {
        if (url == null) {
            imageView.setImageDrawable(placeholder);
        } else {
            MyImageLoader.loadInto(imageView, url, placeholder);
        }
    }

* 바인딩 어댑터의 첫 번째 매개변수는 속성과 연결된 뷰 유형이고, 두 번째 매개변수는 결합 표현식에서 사용되는 유형을 결정한다.

* 두 번째 loadImage 함수와 같이 속성값을 여러개 받아올 수 있다.

* @BindingAdapter 주석은 바인딩 어댑터를 적용할 속성을 인수로 받는다.

- LiveData / ViewModel

LiveData는 관찰자(Activity, Fragment)의 수명 주기를 알고 있다.

따라서 데이터 바인딩과 함께 LiveData 객체를 사용하려면(바인딩 데이터의 자료형이 LiveData여야 함) 수명 주기 소유자를 지정해 LiveData 객체의 범위를 정의해야 한다.

class ViewModelActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            // Inflate view and obtain an instance of the binding class.
            val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

            // Specify the current activity as the lifecycle owner.
            binding.setLifecycleOwner(this)
        }
    }

 

ViewModel의 구성요소에서 LiveData 객체를 사용하여 데이터를 변환할 수 있다.

 

데이터 바인딩에서 ViewModel을 사용하기 위한 과정은 다음과 같다.

  • ViewModel 클래스 인스턴스화
  • 데이터 바인딩 객체 인스턴스화
  • 바인딩 객체의 뷰 모델 속성에 뷰 모델 인스턴스 연결
    class ViewModelActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            // Obtain the ViewModel component.
            val userModel: UserModel by viewModels()

            // Inflate view and obtain an instance of the binding class.
            val binding: UserBinding = DataBindingUtil.setContentView(this, R.layout.user)

            // Assign the component to a property in the binding class.
            binding.viewmodel = userModel
        }
    }
<CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@{viewmodel.rememberMe}"
        android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />
    

 

Observable한 객체와 함께 사용해 바인딩된 데이터가 변경됐을 때의 알림 관린 메서드를 처리할 수 있다.

  /**
     * A ViewModel that is also an Observable,
     * to be used with the Data Binding Library.
     */
    open class ObservableViewModel : ViewModel(), Observable {
        private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()

        override fun addOnPropertyChangedCallback(
            callback: Observable.OnPropertyChangedCallback) {
            callbacks.add(callback)
        }

        override fun removeOnPropertyChangedCallback(
            callback: Observable.OnPropertyChangedCallback) {
            callbacks.remove(callback)
        }

        /**
         * Notifies observers that all properties of this instance have changed.
         */
        fun notifyChange() {
            callbacks.notifyCallbacks(this, 0, null)
        }

        /**
         * Notifies observers that a specific property has changed. The getter for the
         * property that changes should be marked with the @Bindable annotation to
         * generate a field in the BR class to be used as the fieldId parameter.
         *
         * @param fieldId The generated BR id for the Bindable field.
         */
        fun notifyPropertyChanged(fieldId: Int) {
            callbacks.notifyCallbacks(this, fieldId, null)
        }
    }

- 양방향 데이터 결합

단방향 데이터 결합은 다음 코드와 같이 속성에 값을 설정하는 것과 리스너 설정이 각각 이루어지는 것을 말한다.

    <CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@{viewmodel.rememberMe}"
        android:onCheckedChanged="@{viewmodel.rememberMeChanged}"
    />

양방향 데이터 결합은 이 프로세스를 더 간소화한다.

  <CheckBox
        android:id="@+id/rememberMeCheckBox"
        android:checked="@={viewmodel.rememberMe}"
    />

 

"@={}" 표기법은 속성과 관련된 데이터 변경사항을 받는 동시에 사용자 업데이트를 수신 대기한다.

백업 데이터 변경에 대응하기 위한 레이아웃 변수를 Observable 구현으로 만들고 @Bindable 주석을 사용한다.

( 뷰모델의 경우 BaseObservable 클래스를 상속함!)

 

양방향 데이터 결합은 내용이 더 방대하므로 따로 공부해서 포스팅할 예정이다.

 


Reference

- Databinding with ViewModel

https://velog.io/@dustndus8/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9CAndroidKotlin-Activity-Fragment%EC%97%90%EC%84%9C-Databinding-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0ViewModel-%EC%9D%B4%EC%9A%A9

 

[안드로이드/Android/Kotlin] Activity / Fragment에서 Databinding 사용하기(ViewModel 이용)

 

velog.io

https://junghun0.github.io/2019/05/22/android-databinding/

 

[Android] DataBinding 사용해보기 - Junghoon's Blog

Sample : Android Databinding Sample

Junghun0.github.io

https://developer.android.com/topic/libraries/data-binding?hl=ko 

 

데이터 결합 라이브러리  |  Android 개발자  |  Android Developers

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 데이터 결합 라이브러리 Android Jetpack의 구성요소. 데이터 결합 라이브러리는 프로그래매틱 방식이 아니라 선

developer.android.com