간편하고 안전하게 레이아웃 내 뷰를 참조하는 방법: 안드로이드 뷰 바인딩
안드로이드 뷰 바인딩 (Android View Binding)은 뷰를 다루는 코드를 보다 쉽게 작성할 수 있게 해 줍니다. 레이아웃 XML 파일에 정의되어 있는 뷰를 자동으로 생성된 클래스를 통해 자바/코틀린 코드에서 참조할 수 있게 해 주며, 코틀린 안드로이드 익스텐션에서 뷰 접근을 위해 제공하는 합성 프로퍼티 (Synthetic property)와 거의 동일한 기능을 제공합니다.
코틀린 안드로이드 익스텐션은 2021년 내로 지원이 중단될 예정입니다. 따라서, 기존에 코틀린 안드로이드 익스텐션에서 제공하는 뷰 바인딩 기능을 사용하고 계셨던 분들은 이를 안드로이드 뷰 바인딩으로 이전해야 합니다.
뷰 바인딩 활성화하기
별도의 플러그인을 설정해야 했던 코틀린 안드로이드 플러그인과 달리, 안드로이드 뷰 바인딩은 안드로이드 빌드 도구에 통합되어 있기에 별도로 플러그인을 추가할 필요가 없습니다.
단, 기본적으로 뷰 바인딩은 비활성화 되어 있으므로 이를 사용하려면 앱 모듈 빌드스크립트 (일반적으로 app/build.gradle
)에서 이를 활성화 해야 합니다. 다음은 뷰 바인딩 기능을 활성화하는 예를 보여줍니다.
android {
...
// 안드로이드 뷰 바인딩 기능을 활성화 합니다.
buildFeatures {
viewBinding true
}
}
기존에 코틀린 안드로이드 익스텐션을 사용하고 있었다면, 빌드스크립트에서 플러그인을 제거해 줍니다. (apply plugin: 'kotlin-android-extensions'
제거)
뷰 바인딩 동작 원리
모듈 내에 뷰 바인딩이 활성화되면, 해당 모듈 내 포함된 레이아웃 (*.xml) 파일에 상응하는 바인딩 클래스가 생성되며, 이 클래스에는 루트 뷰와 더불어 ID가 뷰여된 모든 뷰에 대한 참조가 포함됩니다.
작명 규칙
바인딩 클래스 이름은 레이아웃 파일 이름에 따라 결정됩니다. 작명 규칙은 다음과 같습니다.
- 레이아웃 파일 이름을 파스칼 케이스 (Pascal case; Upper camel case)로 변환합니다.
- 변환된 이름 뒤에
Binding
을 붙여줍니다.
예를 들어 activity_main.xml
레이아웃 파일이 있는 경우, 이에 상응하는 바인딩 클래스는 ActivityMainBinding
이 됩니다.
뷰 바인딩에서 레이아웃 제외하기
바인딩 클래스를 생성할 필요가 없는 레이아웃이 있다면, 레이아웃 파일의 최상위 뷰에 tools:viewBindingIgnore="true"
를 추가하면 해당 레이아웃에 대한 바인딩 클래스가 생성되지 않도록 설정할 수 있습니다.
다음은 tools:viewBindingIgnore="true"
를 사용하는 예를 보여줍니다.
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>
tools:viewBindingIgnore="true"
속성은 레이아웃 파일의 최상위 뷰에서만 사용할 수 있습니다. 루트 뷰가 아닌 하위 뷰에서는 이 속성이 동작하지 않습니다.
바인딩 클래스 내부 살펴보기
바인딩 클래스 파일은 모듈 내 build/generated/data_binding_base_class_source_out
디렉토리에 생성됩니다.
바인딩 클래스는 자바/코틀린 코드에서 레이아웃 내의 뷰에 접근할 수 있게끔 멤버 필드를 자동으로 생성해 줍니다. 필드 이름은 레이아웃 파일에서 지정한 뷰의 ID와 동일합니다.
다음은 생성된 바인딩 클래스의 예를 보여줍니다. 이 코드는 빌드 과정에서 자동으로 생성됩니다.
// Generated by view binder compiler. Do not edit!
package com.androidhuman.example.viewbinding.databinding;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewbinding.ViewBinding;
import com.androidhuman.example.viewbinding.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;
public final class ActivityMainBinding implements ViewBinding {
@NonNull
private final LinearLayout rootView;
@NonNull
public final Button click;
@NonNull
public final TextView helloText;
private ActivityMainBinding(@NonNull LinearLayout rootView, @NonNull Button click,
@NonNull TextView helloText) {
this.rootView = rootView;
this.click = click;
this.helloText = helloText;
}
@Override
@NonNull
public LinearLayout getRoot() {
return rootView;
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
return inflate(inflater, null, false);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
@Nullable ViewGroup parent, boolean attachToParent) {
View root = inflater.inflate(R.layout.activity_main, parent, false);
if (attachToParent) {
parent.addView(root);
}
return bind(root);
}
@NonNull
public static ActivityMainBinding bind(@NonNull View rootView) {
// The body of this method is generated in a way you would not otherwise write.
// This is done to optimize the compiled bytecode for size and performance.
int id;
missingId: {
id = R.id.click;
Button click = rootView.findViewById(id);
if (click == null) {
break missingId;
}
id = R.id.hello_text;
TextView helloText = rootView.findViewById(id);
if (helloText == null) {
break missingId;
}
return new ActivityMainBinding((LinearLayout) rootView, click, helloText);
}
String missingId = rootView.getResources().getResourceName(id);
throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}
}
위의 바인딩 클래스는 아래 레이아웃 파일을 기반으로 생성되었습니다. 아래 코드와 위의 코드를 번갈아 확인해보면, 바인딩 클래스가 어떻게 구성되는지 조금 더 잘 이해할 수 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/hello_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
<Button
android:id="@+id/click"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Click me!" />
</LinearLayout>
액티비티에서 사용하기
액티비티에서 뷰 바인딩을 사용하려면 액티비티 내 onCreate()
에서 바인딩 클래스를 설정해야 합니다. 설정 절차는 다음과 같습니다.
- 생성된 바인딩 클래스의
inflate()
메서드를 사용하여 액티비티에서 사용할 바인딩 클래스의 인스턴스를 생성합니다. - 바인딩 클래스의
getRoot()
메서드를 통해 레이아웃 내 최상위 뷰의 인스턴스를 얻습니다. setContentView()
메서드에 이전 단계에서 획득한 최상위 뷰의 인스턴스를 넘겨줍니다.
다음은 액티비티에서 뷰 바인딩을 설정하는 예를 보여줍니다.
class MainActivity : AppCompatActivity() {
// 액티비티에서 사용할 레이아웃의 뷰 바인딩 클래스
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 뷰 바인딩 클래스의 인스턴스를 생성합니다.
binding = ActivityMainBinding.inflate(layoutInflater)
// 생성된 뷰를 액티비티에 표시합니다.
setContentView(binding.root)
}
}
이제 뷰 바인딩 클래스 인스턴스로 레이아웃 내 뷰를 참조할 수 있습니다. 다음은 간단한 예를 보여줍니다.
// showSecondActibity 버튼에 클릭 리스터를 지정합니다.
binding.showSecondActivity.setOnClickListener {
startActivity(Intent(this, SecondActivity::class.java))
}
프래그먼트에서 사용하기
프래그먼트에서 뷰 바인딩을 사용하려면 프래그먼트 내 onCreateView()
에서 바인딩 클래스를 설정해야 합니다. 설정 절차는 다음과 같습니다.
- 생성된 바인딩 클래스의
inflate()
메서드를 사용하여 액티비티에서 사용할 바인딩 클래스의 인스턴스를 생성합니다. - 바인딩 클래스의
getRoot()
메서드를 통해 레이아웃 내 최상위 뷰의 인스턴스를 얻습니다. onCreateView()
메서드의 반환값으로 이전 단계에서 획득한 최상위 뷰의 인스턴스를 넘겨줍니다.
다음은 프래그먼트에서 뷰 바인딩을 설정하는 예를 보여줍니다.
class SecondFragment : Fragment() {
private var _binding: FragmentSecondBinding? = null;
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentSecondBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
프래그먼트는 뷰에 비해 수명이 더 깁니다. 바인딩 클래스는 뷰에 대한 참조를 가지고 있으므로, 뷰가 제거될 때 호출되는
onDestroyView()
콜백 내에서 바인딩 클래스의 인스턴스도 함께 정리해 주어야 합니다.
뷰 바인딩과 findViewById는 무엇이 다른가요?
사실, findViewById()
를 사용해도 액티비티 혹은 프래그먼트 내 뷰를 참조할 수 있습니다. 하지만, 뷰 바인딩은 findViewById()
대비 다음과 같은 장점이 있습니다.
- 널 안전 (Null Safety): 바인딩 클래스에서 제공하는 필드는 각 뷰를 직접 참조하게 구성되어 있습니다. 따라서, 잘못된 ID를 대입하여 널 포인터 오류가 발생하는 등의 문제가 일어나지 않습니다. 또한, 특정 구성 (configuration)에서만 접근할 수 있는 뷰가 있는 경우 이는
@Nullable
로 표시되므로 뷰 참조시 실수를 방지할 수 있습니다. - 타입 안정성 (Type safecy): 바인딩 클래스 내 필드는 레이아웃 내 선언된 뷰의 타입을 갖습니다. 따라서 잘못된 타입으로 캐스팅 (예:
ImageView
를TextView
로 캐스팅)하는 실수를 원천 봉쇄할 수 있습니다.
이러한 특성 덕분에, 레이아웃과 코드 사이에 일치하지 않는 부분이 있다면 컴파일 오류가 발생하게 됩니다. 따라서, 앱 배포 후 발생할 수 있는 런타임 오류를 미연에 방지할 수 있습니다.
이 글에서 사용한 예제 프로젝트 소스 코드는 Github 내 kunny/blog_samples 저장소에서 확인할 수 있습니다.