[Android] View Binding (뷰 바인딩)

  • 뷰 바인딩(View Binding) 기능은 뷰와 상호작용하는 코드를 쉽게 작성할 수 있게 해준다.
  • 모듈에서 사용 설정(enable)된 뷰 바인딩은 각 XML 레이아웃 파일의 바인딩 클래스(binding class)를 생성한다.
  • 바인딩 클래스의 인스턴스는 상응하는 레이아웃에 ID가 있는 모든 뷰의 직접 참조가 포함된다.
  • 대부분의 경우, 뷰 바인딩이 findViewById를 대체한다.

설정 방법

뷰 바인딩은 모듈 별로 사용 설정이 된다(enabled on a module by module).

모듈에서 뷰 바인딩을 사용 설정(enable) 하려면, module 레벨의 build.gradle 파일에 viewBinding 빌드 옵션을 아래의 예시와 같이 true로 변경한다.

1
2
3
4
5
6
android {
...
viewBinding {
enabled = true
}
}

바인딩 클래스를 생성하는 동안 레이아웃 파일을 무시하려면 tools:viewBindingIgnore="true" 속성을 레이아웃 파일의 루트 뷰에 추가해야 한다.

사용법

모듈에 뷰 바인딩이 사용 설정되면, 모듈에 포함된 각 XML 레이아웃 파일의 바인딩 클래스가 생성된다.

각 바인딩 클래스에는 루트 뷰와 ID가 있는 모든 뷰에 대한 참조를 포함한다.

생성된 바인딩 클래스의 이름은 XML 파일의 이름을 카멜 표기법으로 변환하고 끝에 'Binding’이 추가된다.

result_profile.xml 이름을 가진 레이아웃 파일의 예시

1
2
3
4
5
6
<LinearLayout ... >
<TextView android:id="@+id/name" />
<ImageView android:cropToPadding="true" />
<Button android:id="@+id/button"
android:background="@drawable/rounded_button" />
</LinearLayout>

생성된 바인딩 클래스의 이름은 ResultProfileBinding이 된다. 이 클래스에는 name이라는 TextViewbutton이라는 Button 등 두 개의 필드가 있다. 레이아웃의 ImageView에는 ID가 없으므로 바인딩 클래스에 참조가 없다.

모든 바인딩 클래스는 getRoot() 메서드를 포함하고 있는데, 상응하는 레이아웃 파일의 루트 뷰에 대한 직접 참조를 제공한다.

위의 예시 코드에서는 ResultProfileBinding 클래스의 getRoot() 메서드가 LinearLayout 루트 뷰를 반환한다.

액티비티에서의 뷰 바인딩 사용법

액티비티에 사용할 바인딩 클래스 인스턴스를 설정하려면, 액티비티의 onCreate() 메서드에서 다음 두 단계를 따라야 한다.

  1. 생성된 바인딩 클래스에 포함된 static inflate() 메서드를 호출한다. 이를 통해 액티비티에서 사용할 바인딩 클래스의 인스턴스를 생성한다.
  2. getRoot()메서드를 호출하거나 Kotlin property syntax를 사용하여 루트 뷰의 참조를 가져온다.
  3. 루트 뷰를 setContentView()에 전달(pass)하여 화면 상의 활성 뷰로 만든다.
1
2
3
4
5
6
7
8
private lateinit var binding: ResultProfileBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ResultProfileBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
}

이제 바인딩 클래스의 인스턴스를 사용하여 뷰를 참조할 수 있다.

1
2
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }

프래그먼트에서의 뷰 바인딩 사용법

프래그먼트에서 사용할 바인딩 클래스의 인스턴스를 설정하려면, 프래그먼트의 onCreateView() 메서드에서 다음 단계를 따라야 한다.

  1. 생성된 바인딩 클래스에 포함된 static inflate() 메서드를 호출한다. 그러면 프래그먼트에서 사용할 바인딩 클래스의 인스턴스가 생성된다.
  2. getRoot()메서드를 호출하거나 Kotlin property syntax를 사용하여 루트 뷰의 참조를 가져온다.
  3. onCreateView() 메서드에서 루트 뷰를 반환하여 화면 상의 활성 뷰를 만든다.

★ 참고 : inflate() 메서드를 사용하려면 layout inflator를 전달해야 한다. 레이아웃이 이미 inflate 되었다면, 바인딩 클래스의 static bind() 메서드를 호출하면 된다. 자세한 내용은 본문의 하단이나 뷰 바인딩 깃허브 샘플의 예시에서 볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

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

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

이제 바인딩 클래스의 인스턴스를 사용하여 뷰를 참조할 수 있다.

1
2
binding.name.text = viewModel.name
binding.button.setOnClickListener { viewModel.userClicked() }

★ 참고 : 프래그먼트는 뷰보다 오래 지속된다(Fragments outlive their views). 프래그먼트의 onDestroyView() 메서드에서 바인딩 클래스 인스턴스에 대한 참조를 정리해야 한다.

다른 구성(configuration)에 대한 힌트

여러 구성(configuration)에서 뷰를 선언할 때, 특정 레이아웃에 따라 다른 뷰 타입을 사용하는 것이 더 합리적이다.

1
2
3
4
5
# in res/layout/example.xml
<TextView android:id="@+id/user_bio" />

# in res/layout-land/example.xml
<EditText android:id="@+id/user_bio" />

이러한 경우, TextView가 공통된 기본 클래스(common base class)이기 때문에 생성된 클래스가 TextView 타입의 userBio 필드를 노출할 것으로 예상할 것이다. 하지만 기술적인 한계로 인해 뷰 바인딩 코드 생성기는 이러한 결정을 내릴 수 없으며, 대신에 단순히 View 필드를 생성한다. 이를 위해서는 나중에 binding.userBio as TextView를 사용하여 필드를 캐스팅해야 한다.

이 제한 사항을 해결하기 위해, 뷰 바인딩은 tools:viewBindingType 속성을 지원하여 생성된 코드에서 어떤 타입을 사용할 것인지 컴파일러에게 알릴 수 있다.

tools:viewBindingType 속성을 사용하여 컴파일러가 필드를 TextView로 생성하게 하기

1
2
3
4
5
# in res/layout/example.xml (unchanged)
<TextView android:id="@+id/user_bio" />

# in res/layout-land/example.xml
<EditText android:id="@+id/user_bio" tools:viewBindingType="TextView" />

다른 예시로, 하나는 BottomNavigationView를 포함하고 다른 하나는 NavigationRailView를 포함하는 두 개의 레이아웃이 있다고 가정해보자. 두 클래스 모두 구현의 세부 정보가 포함된 NavigationBarView를 확장한다. 코드가 현재 레이아웃에 어떤 하위 클래스가 있는지 정확히 알 필요가 없는 경우, tools:viewBindingType를 사용하여 생성된 타입을 두 레이아웃 모두에서 NavigationBarView로 설정할 수 있다.

1
2
3
4
5
# in res/layout/navigation_example.xml
<BottomNavigationView android:id="@+id/navigation" tools:viewBindingType="NavigationBarView" />

# in res/layout-w720/navigation_example.xml
<NavigationRailView android:id="@+id/navigation" tools:viewBindingType="NavigationBarView" />

참고로 뷰 바인딩은 코드를 생성할 때 속성 값의 유효성을 검사할 수 없다. 컴파일 타임과 런타임 오류를 방지하려면 값이 다음 조건들을 충족해야 한다.

  • 값은 android.view.View에서 상속되는 클래스여야 한다.
  • 값은 해당 값이 배치된 태그의 슈퍼 클래스여야 한다. 예를 들어 다음 값들은 작동하지 않는다.
    • <TextView tools:viewBindingType="ImageView" /> : ImageView는 TextView와 관련이 없다.
    • <TextView tools:viewBindingType="Button" /> : Button은 TextView의 슈퍼 클래스가 아니다.
  • 최종 타입은 모든 구성에서 일관되게 해결되어야 한다.

findViewById와의 차이점

뷰 바인딩은 findViewById를 사용하는 것에 비해 중요한 장점이 있다.

  • 널 안정성
    • 뷰 바인딩운 뷰에 대한 직접 참조를 생성하므로, 유효하지 않은 view ID로 인해 null pointer exception이 발생할 위험이 없다. 또한 레이아웃의 일부 구성에서만 뷰가 있는 경우, 바인딩 클래스에서 참조를 포함하는 필드가 @Nullable로 표시된다.
  • 타입 안정성
    • 각 바인딩 클래스에 있는 필드는 XML 파일에서 참조하는 뷰와 일치하는 타입을 가진다. 즉, 클래스 변환 예외(class cast exception)이 발생할 위험이 없다.

이러한 차이점은 레이아웃과 코드 사이의 비호환성으로 인해 findViewById가 런타임에 오류가 발생하는 반면, 뷰 바인딩은 런타임이 아닌 컴파일 타임에 빌드가 실패하게 된다는 것을 의미한다.

연산 속도 면에서도 findViewById는 레이아웃 태그를 순회하여 일치하는 뷰를 찾아가기 때문에 연산 속도에 영향을 미치고, 단순 바인딩 코드가 길어진다.

데이터 바인딩과의 비교

뷰 바인딩과 데이터 바인딩은 모두 뷰를 직접 참조하는 데 사용할 수 있는 바인딩 클래스를 생성한다. 하지만 뷰 바인딩은 보다 단순한 사용 사례를 처리하기 위한 것이며 데이터 결합에 비해 다음과 같은 이점을 제공한다.

  • 더 빠른 컴파일
    • 뷰 바인딩은 주석 처리(annotation processing)이 필요하지 않으므로 컴파일 시간이 더 짧다.
  • 사용 편의성
    • 뷰 바인딩은 특별히 태그된 XML 레이아웃 파일이 필요하지 않으므로 앱에서 더 신속하게 채택될 수 있다. 모듈에서 뷰 바인딩을 사용 설정하면 모듈의 모든 레이아웃에 뷰 바인딩이 자동으로 적용된다.

반대로 뷰 바인딩에는 데이터 바인딩과 비교해서 다음과 같은 제한 사항들이 있다.

위 사항을 고려할 때, 일부 사례에서는 뷰 바인딩과 데이터 바인딩을 모두 사용하는 것이 가장 좋다. 고급 기능이 필요한 레이아웃에는 데이터 바인딩을, 고급 기능이 필요 없는 레이아웃에는 뷰 바인딩을 사용할 수 있다.

레이아웃이 이미 인플레이트된 상황에서의 뷰 바인딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* View Binding example with a fragment that uses the alternate constructor for inflation and
* [onViewCreated] for binding.
*/
class BindFragment : Fragment(R.layout.fragment_blank) {

// Scoped to the lifecycle of the fragment's view (between onCreateView and onDestroyView)
private var fragmentBlankBinding: FragmentBlankBinding? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentBlankBinding.bind(view)
fragmentBlankBinding = binding
binding.textViewFragment.text = getString(string.hello_from_vb_bindfragment)
}

override fun onDestroyView() {
// Consider not storing the binding instance in a field, if not needed.
fragmentBlankBinding = null
super.onDestroyView()
}
}
  • 위 코드는 인플레이션을 위해 Alternate constructor를 사용하고 onViewCreated를 바인딩에 사용하는 프래그먼트의 뷰 바인딩 예시이다. 레이아웃이 이미 인플레이트 되었기에 바로 바인딩 클래스의 static bind() 메서드를 호출하면 된다.

Additional resources

Samples

Blogs

Videos

References

댓글