[Android] ViewBinding (Kotlin)

junsuPark·2022년 11월 8일
0

Android

목록 보기
4/12
post-thumbnail

ViewBinding

ViewBinding ?

ViewBinding이란, View Controller에서 XML의 위젯에 보다 쉽고 안전하게 접근하기 위해 사용하는, 단방향 View 접근을 도와주는 기능입니다.

ViewBinding 이전에 사용된 보일러플레이트 코드 제거 방법들

ViewBinding은 안드로이드 스튜디오 버전 3.6부터 지원하기 시작했습니다. 버전 3.6 이전까지는 ButterKnife 등의 라이브러리 또는 extension을 사용하여 findViewById() 보일러플레이트 코드를 제거했습니다.

이후 ViewBinding과 비슷하지만, 더욱 간소화된 Kotlin Synthetic이란 extension 출시했습니다. 하지만 전역적으로 사용되는 네임스페이스에 부하를 주어 Deprecated 되었습니다. 또한 동일한 ID를 사용할 시 NPE(NullPointerException)이 발생할 수 있습니다.

특징

ViewBinding은, 각 XML 레이아웃의 바인딩 클래스를 자동으로 생성하여 XML 레이아웃의 위젯에 대한 접근을 클래스로써 가능하게 해줍니다.

ViewBinding을 사용하지 않는 클래스에서 XML 레이아웃의 위젯을 참조하기 위해서는, findViewById() 메소드를 사용하여 변수에 직접 저장하는 방식을 사용합니다. 하지만 XML 레이아웃에 ID로 접근 가능해야 하는 위젯들이 추가될수록 findViewById() 보일러플레이트 코드의 부피 또한 팽창하며, 무엇보다 XML 레이아웃의 위젯과 바인딩할 변수의 양도 비례하게 늘어납니다. 이를 해결하기 위해 ViewBinding을 사용하여 XML 레이아웃을 클래스화하여 간편한 참조를 가능하게 합니다.

  • XML 내부에 ID가 존재하는 모든 위젯을 클래스의 속성으로 가집니다
  • XML 레이아웃의 이름을 참조하여 클래스와 멤버 변수가 생성됩니다
    • XML 레이아웃 또는 내부 위젯의 ID 이름에서 언더바를 제거하고, 단어의 첫 글자에 카멜케이스 문법을 적용합니다
      • Class Name : activity_main -> ActivityMainBinding
      • Property Name : tv_main_primary -> tvMainPrimary

ViewBinding vs findViewById()

findViewById()를 이용한 XML 레이아웃 위젯 접근 방식

<TextView
        android:id="@+id/tv_main_secondary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView Secondary" />
val tvMainSecondary: TextView = findViewById(R.id.tv_main_secondary)

기존 안드로이드에서 XML의 위젯에 접근하기 위해서는 XML에서 위젯과 ID를 선언한 후, View Controller에서 findViewById() 메서드를 사용하여 View 객체를 참조할 수 있었습니다. findViewById() 메서드는 파라미터로 XML 레이아웃 위젯의 ID를 받으며, 해당 위젯을 inflate한 View 객체를 반환합니다.

Null Issue

findViewById()를 사용하여 XML 레이아웃의 위젯을 참조하면 잘못된 참조 설정으로 인해 NPE(NullPointerException)이 발생할 위험이 있습니다. 주로 존재하지 않는 ID의 참조 및 반환으로 인해 발생합니다.

Type Cast Issue

findViewById() 메소드로 XML 레이아웃의 위젯을 참조하여 변수에 저장할 때, 해당 변수에 타입을 명시하여야 합니다.

val tvMainSecondary: TextView = findViewById(R.id.tv_main_secondary)

하지만 잘못된 타입 명시로 인해 ClassCastException이 발생할 수 있습니다.
아래 스니펫에서는 XML 레이아웃에서는 TextView로 선언되어 있으나 Button으로 잘못 캐스팅한 예시를 표현합니다.

val tvMainSecondary: Button = findViewById(R.id.tv_main_secondary)
java.lang.ClassCastException: com.google.android.material.textview.MaterialTextView cannot be cast to android.widget.Button

보일러플레이트 코드 팽창

findViewById() 메소드를 사용하여 XML 레이아웃의 위젯을 참조할 때, 사용할 모든 위젯에 대한 변수와의 바인딩으로 findViewById() 보일러플레이트 코드가 매 참조마다 발생합니다. 또한, 여러 XML 레이아웃에서 동일한 ID를 사용하면 참조 오류가 발생할 수 있습니다.


ViewBinding을 이용한 XML 레이아웃 위젯 접근 방식

<TextView
        android:id="@+id/tv_main_secondary"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView Secondary" />
val binding: ActivityMainBinding by lazy {
    ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
}

ViewBinding을 사용하면 해당 XML 레이아웃을 클래스화하여 XML 레이아웃 내부에 ID가 명시된 위젯들을 클래스의 멤버 변수로 참조하여 사용할 수 있습니다. 즉, 해당 XML 레이아웃 전체를 inflate하며, 해당 XML 레이아웃이 클래스화 된 Binding 클래스의 인스턴스를 자동으로 생성하여 XML 레이아웃 최상위 태그를 포함한 ID가 명시된 모든 위젯들을 클래스의 멤버 변수로 가지게 됩니다.

Null-Safety

Binding 클래스의 멤버 변수로 생성되는 View 객체들은 모두 원본 XML 레이아웃의 태그와 ID를 참조하여 생성되므로, 존재하지 않는 ID의 참조가 발생할 수 없습니다. 따라서 NPE(NullPointerException)이 발생하지 않습니다.

Type-Safety

Binding 클래스의 멤버 변수로 생성되는 View 객체들은 모두 원본 XML 레이아웃의 태그와 ID를 참조하여 생성되므로, 모든 멤버 변수들은 XML 레이아웃의 태그를 참조하여 View 타입이 결정됩니다. 따라서 잘못된 타입으로 인한 Exception이 발생하지 않습니다.

코드 간소화

XML 레이아웃의 위젯과 View 변수의 바인딩을 위한 findViewById() 등의 보일러플레이트 코드가 제거되기 때문에, 전체적인 코드의 길이가 줄어듭니다. 또한, 개발자가 일일히 보일러플레이트 코드인 findViewById() 작성할 필요가 없습니다. 또한, 여러 XML 레이아웃에서 동일한 ID를 사용하더라도 각 XML 레이아웃의 Binding 클래스 내의 멤버 변수를 참조하여 사용하기 때문에, 참조에 오류가 발생하지 않습니다.


ViewBinding > findViewById()

findViewById()를 사용하여 View Controller에서 View에 접근하면 XML 레이아웃의 위젯을 선언할 시 직관적이며, View Controller에서 해당 위젯을 관리한다는 점이 있지만, 이는 모두 ViewBinding을 사용하여 관리할 수 있는 기능입니다. 즉, findViewById()는 ViewBinding이 대체할 수 있습니다.

findViewById()는, XML 레이아웃에서 해당 ID를 가진 위젯을 찾을 때까지 모든 ViewGroup을 순회하며 깊이 우선 검색(DFS) 탐색을 수행하기 때문에 많은 태그에 속해있는 위젯을 참조하는 작업을 수행할 때 느린 성능을 보입니다. 하지만 ViewBinding은 XML 레이아웃을 클래스로 변환하여 참조하기 때문에, 부모로 가지는 ViewGroup의 개수와 상관없이 일정한 참조 속도를 보여 훨씬 빠른 성능을 보입니다.

또한 앞서 언급했듯, 위젯과 변수를 바인딩하기 위한 findViewById()의 보일러플레이트 코드 작성이 생략됩니다. 만일 ViewBinding에서 findViewByid() 메서드를 사용한다면, 이미 inflate되어 있을 수 있는 View를 메모리에 다중 할당하므로 비효율적입니다.

단순히 findViewById()를 대체하기 위한 목적으로 ViewBinding을 사용합니다. findViewById() 대신 ViewBinding을 사용하면 빠른 성능과 보일러플레이트 코드를 제거할 수 있습니다.


ViewBinding의 사용

gradle 설정

android {
    ...
    buildFeatures {
        viewBinding true
    }
    ...
}

View Controller

Activity

class MainActivity : AppCompatActivity() {

    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
    }
}

필자는 binding 변수를 전역으로 선언하여 lazy로 늦은 초기화를 하는 방식을 택했습니다. lateinit var 등 다른 방식을 사용하여도 됩니다.

Fragment

class MainFragment : Fragment() {

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

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        _binding = FragmentMainBinding.inflate(inflater, container, false)
        return binding.root
    }

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

Fragment의 경우 onDetach() 생명주기 메소드까지 수행하며, Fragment를 담고 있는 Activity보다 생명 시간이 길게 유지됩니다. 자연스레 Memory Leak이 발생할 수 있으며, 이를 방지하기 위해 Fragment의 onDestroyView() 메소드에서 binding을 담고 있는 변수를 null로 정의하여 Memory Leak을 방지합니다.


특정 XML 레이아웃에서 ViewBinding 사용 해제

<androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:viewBindingIgnore="true" />

프로젝트 App 모듈에서 ViewBinding을 활성화 했지만, 특정한 XML 레이아웃에서 ViewBinding을 사용 해제할 때는 tools:viewBindingIgnore="true" 속성을 정의해준다. 이 때, 해당 XML 레이아웃의 Binding 클래스가 생성되지 않는다.


참고 자료

0개의 댓글