[Android-공식문서 파헤치기] Architecture Component(2) - View Binding

이동현·2020년 7월 9일
0

본 시리즈는 안드로이드 아키텍처를 이해하기 위해 Android 공식문서를 읽고 정리한 글의 모음입니다.
본문에서의 예제는 공식문서내의 예제를 대부분 사용합니다.
공부하면서 작성하는 글이기 때문에 내용상 오류가 있을 가능성이 매우 높습니다.
https://developer.android.com/topic/libraries/view-binding


View Bindng - 도입

뷰 바인딩은 view와 상호작용하는 코드를 더욱 쉽게 작성할 수 있도록 도와주는 기능입니다.
잘 와닿지가 않은데, xml파일을 보면 각각의 id를 가진 여러 view들이 그 파일을 이루는 것을 볼 수 있습니다.

findViewById()의 문제점

예전에 java로 안드로이드 공부할 때 findViewById()를 사용하여 각 view component들을 객체로 만들어 사용했었던 기억이 납니다. 비로서 이런 과정을 거쳐야 코드단에서 view를 컨트롤 할 수 있었기 때문이죠. 코틀린에서도 마찬가지입니다. (참고로, Kotlin Android Extension에서는 findViewById()를 사용하지 않아도 됩니다. synthetic binding을 지원하기 때문입니다.)

// java
TextView textView = (TextView) findViewById(R.id.tv_first)

// kotlin
val textView = findViewById<TextView>(R.id.tv_first)

위와 같은 방법에는 몇 가지 문제점(단점)이 존재합니다.

  • 만약 사용하는 컴포넌트가 10개만 되어도, 위와 같은 코드는 10줄이 추가됩니다. (100개라면..?!)
  • findViewById 함수는 사용하기에 꽤나 무거운 함수입니다.
  • null값에 상대적으로 안전하지 못합니다. 존재하지 않는 컴포넌트/id를 불러올 수 있기 때문입니다.

레이아웃의 id를 바로 사용하자

위의 문제점들은 안드로이드 앱 개발자들에게 상당한 골칫거리였다고 합니다. (저는 경험 안해봤네요)
만약 레이아웃의 컴포넌트를 객체화 시키지 않고 바로 사용할 수 있다면, 불필요한 코드 작업을 덜 할 수 있지 않을까요? 그런 생각으로 도입 된 것이 ViewBinding입니다.

Once view binding is enabled in a module, it generates a binding class for each XML layout file present in that module. An instance of a binding class contains direct references to all views that have an ID in the corresponding layout. In most cases, view binding replaces findViewById.

View Binding은 모듈별로 사용 설정 됩니다. 사용 설정이 됐다면 각 xml 파일에 대해 Binding Class를 자동 생성합니다. Binding Class의 인스턴스는 ID를 가지고 있는 모든 뷰에 대한 직접적 참조를 포함합니다. 그에 따라 findViewById를 사용하지 않고 대체 할 수 있다는 뜻입니다.

Binding Class에 존재하는 참조를 코드단에서 바로 사용하여 불필요한 findViewById를 사용하지 않는다는 이야기가 됩니다.

(코틀린은 BindingClass의 생성 없이 layout의 id값을 참조하여 view를 사용합니다. 이것은 ViewBinding과 다른 관점에서 봐야하며, 이에 대한 비교 포스팅은 추후에 하도록 하겠습니다.)

View Binding - Usage

module level의 build.gradle에서 viewBinding을 허용해줍니다.

android {
    ...
    buildFeatures {
        viewBinding true
    }
}

추가적으로, viewBinding은 앞서 언급했듯 모듈단에서 전체 레이아웃을 대상으로 이루어지므로, 원하지 않는 layout에 대해서는 제한을 두어야합니다.

<LinearLayout
        ...
        tools:viewBindingIgnore="true" >
    ...
</LinearLayout>

ViewBinding Class의 생성

ViewBinding Class는 다음의 규칙을 가지며 생성됩니다.

  • 빌드 과정에서 생성됩니다.
  • Binding단어가 클래스의 이름 끝에 붙습니다.
  • Camel 표기법에 따라 naming됩니다.
  • ID가 존재하지 않는 View에 대해서는 클래스에 참조가 존재하지 않습니다.
  • getRoot()메서드가 자동 포함됩니다. 이 메서드는 레이아웃 파일의 루트 뷰에 관한 직접 참조를 제공합니다.

공식문서의 예제를 예로 들겠습니다.
아래의 코드는 result_profile.xml 레이아웃 파일입니다.

<LinearLayout ... >
    <TextView android:id="@+id/name" />
    <ImageView android:cropToPadding="true" />
    <Button android:id="@+id/button"
        android:background="@drawable/rounded_button" />
</LinearLayout>

위의 규칙에 따라 생성되는 ViewBinding Class의 이름은 ResultProfileBinding이 됩니다.
또한, ImageView에 대한 id가 존재하지 않으므로, 그에 따른 참조는 없습니다.
자동 생성되는 getRoot() 메서드의 반환값은 부모 뷰인 LinearLayout이 됩니다.

Activity에서의 ViewBinding 사용

Activity에서 사용할 BindingClass의 인스턴스를 설정하기 위해 onCreate() 메서드에서 다음 과정을 거칩니다.

  1. BindingClass의 inflate()를 호출하여 인스턴스 생성
  2. getRoot()를 사용하여 루트 뷰의 참조를 획득
  3. setContentView()에 루트 뷰를 전달하여 화면상의 활성 뷰로 만들어 줌
private lateinit var binding: ResultProfileBinding

// onCreate()에서 작업
override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(savedInstanceState)
    // 1. inflate 호출하여 인스턴스 생성
    binding = ResultProfileBinding.inflate(layoutInflater)
    
    // 2. getRoot()
    val view = binding.root
    
    // 3. 화면상의 활성 뷰로 만든다.
    setContentView(view)
}

이제, BindingClass의 인스턴스를 사용하여 View를 참조할 수 있습니다.

// name의 id를 가진 TextView에 setText() 함수 사용
binding.name.text = viewModel.name
// button의 id를 가진 Button에 ClickListener 설정
binding.button.setOnClickListener { viewModel.userClicked() }

Fragment에서의 ViewBinding 사용

Fragment에서 사용할 BindingClass의 인스턴스를 설정하기 위해 onCreateView() 메서드에서 다음 과정을 거칩니다.

  1. BindingClass의 inflate()를 호출하여 인스턴스 생성
  2. getRoot()를 사용하여 루트 뷰의 참조를 획득
  3. onCreateView()에서 루트 뷰를 반환하여 화면상의 활성 뷰로 만들어 줌
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

// onCreateView()에서 작업
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // 1. inflate()
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    // 2. getRoot()
    val view = binding.root
    // 3. 부모 뷰 리턴
    return view
}

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

이제, BindingClass의 인스턴스를 사용하여 View를 참조할 수 있습니다.

// name의 id를 가진 TextView에 setText() 함수 사용
binding.name.text = viewModel.name
// button의 id를 가진 Button에 ClickListener 설정
binding.button.setOnClickListener { viewModel.userClicked() }

Differences from findViewById

성능적인 특성으로 보았을 때 findViewById와 다른점이 아래와 같이 존재합니다.

  • Null Safety - 뷰의 직접 참조를 가지는 클래스를 생성하므로 유효하지 않은 뷰의 id로 인해 NPE 발생 위험이 없습니다.
  • Type Safety - Binding Class에 존재하는 필드들이 뷰의 타입과 일치하므로 Class Cast Exception의 발생 위험이 없습니다.

이러한 차이점들이 나타내는 바는 layout과 코드단의 비호환성에 대한 검사가 compile 단계에서 이루어진다는 것입니다.

Comparison with DataBinding

(DataBinding에 대한 포스팅은 뒤이어 할 것 같습니다.)

ViewBinding과 DataBinding은 모두 뷰를 직접 참조하는데 사용할 수 있는 BindingClass를 제공합니다. ViewBinding은 보다 단순한 경우를 처리하기 위함에 적합하며 DataBinding과 비교했을 때 다음과 같은 이점을 제시합니다.

  • 더 빠른 컴파일 - ViewBinding에서는 annotation처리가 필요하지 않으므로 컴파일이 더 빠릅니다.
  • 사용의 편의성 - 특별한 tag로 처리된 xml이 필요하지 않으므로 사용이 용이합니다. 모듈에서 Binding의 사용 설정만을 통해 자동으로 모든 레이아웃의 BindingClass가 생성됩니다.

하지만, DataBinding과 비교했을 때의 불리함 역시 존재합니다.

  • ViewBinding은 layout에서의 표현식 혹은 변수를 제공하지 않으므로 동적인 UI 컨텐츠를 생성할 수 없습니다.
  • two-way data binding을 제공하지 않습니다.
profile
영차영차

0개의 댓글