본문으로 바로가기

Android view binding (뷰 바인딩)

category 개발이야기/Android 2020. 3. 4. 10:40
반응형

View bindind이 Android studio 3.6부터 정식지원 됩니다.

물론 3.6 Canary 11 버전에서부터 지원했지만, 현재 정식 3.6 버전이 나왔기에 정식버전으로 사용하시면 됩니다.

 

이 글은 하기 링크를 참고, 번역, 의역하였습니다.

https://medium.com/androiddevelopers/use-view-binding-to-replace-findviewbyid-c83942471fc

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

 

View binding 이란?

간단하게 findViewById를 쓰지 않고, XML의 view component에 접근하는 object를 반환받아 view에 접근하는 방식입니다.
여기서 말하는 object는 Android studio에서 자동으로 대신 만들어줍니다.
 

간략하게 정리하면

  • build.gradle에서 enable 시킵니다. (추가 libarary가 필요하지 않습니다.)
  • 모든 layout에 대해서 binding object가 생성됩니다.
  • Binding object는 id를 갖는 모든 view들을 하나의 property로 가집니다.
    • type도 알아서 맞춰서 생성해 주고, findViewById에서 발생할 수 있는 null도 발생하지 않습니다. (null-safety)
  • Java, kotlin 모두를 지원합니다.
 

build.gradle 설정

View binding은 Android Studio 3.6에 있는 Android Gradle Plugin에 포함되어 추가 라이브러리 없이 사용이 가능합니다.
따라서 아래와 같이 build.gradle file만 수정해 주면 됩니다.
// Available in Android Gradle Plugin 3.6.0 android {     viewBinding {         enabled = true     } } 

 

Android Studio 4.0 부터는 이 param의 위치가 buildFeatures로 이동되었기에 아래와 같이 사용합니다.

 

// Android Studio 4.0 android {     buildFeatures {         viewBinding = true     } } 

 

일단 project에서 이 값만 enable 시켜놓는다면 view binding은 모든 layout에 대한 binding class를 자동 생성합니다.

즉, 그외 XML에 뭘 추가해야 한다던가 하는 작업이 없습니다.

FragmentActivity, Recyclerview의 adapter(또는 ViewHolder)등의 layout을 inflate할때 언제든지 binding class를 사용할 수 있습니다.

 


 

Use view binding in an Activity

Activity에서 사용하는 xml에 아래와 같은 항목이 있다고 가정합니다.
  • activity_awesome.xml
  • 두개의 TextView (id가 각각 title, subtitle)
  • 하나의 Button (id = button
이 세개의 view들은 Activity에서 실제로 아래와 같이 사용가능합니다.
 
override fun onCreate(savedInstanceState: Bundle?) {     super.onCreate(savedInstanceState)     val binding = ActivityAwesomeBinding.inflate(layoutInflater)      binding.title.text = "Hello"     binding.subtext.text = "Concise, safe code"     binding.button.setOnClickListener { /* ... */ }      setContentView(binding.root) } 

 

예제에서보면 xml의 이름이 카멜 대소문자 형태로 변경되어 코드에 사용됩니다.

이렇게 얻은 객체에는 각 view의 id가 property로 제공됩니다.

따라서 더이상 findViewById를 사용할 필요가 없겠죠?

 

특히 xml 전체를 감싸는 최상단의 부모를 root 라는 property로 제공합니다.

따라서 이 속성은 setContentView의 인자로 넘겨주어 쉽게 사용할 수 있습니다.

사실 setContentView의 인자로 root를 사용하는건 선택이 아니라 필수적인 요소 입니다.
만약 view binding도 사용하고, setContentView 기존과 같은 방식으로 layout resource id를 넘겨준다면, 두번의 layout inflate 가 발생 됩니다.
또한 이렇게 생성된 root layout object는 서로 다른 객체이니 각각 가르키는 view가 다르니 원하는대로 동작하지 않겠죠?
 
정리하면, view biding을 사용할 경우 setContentView에는 반드시 binding 객체의 root를 넘겨줘야 합니다.

 

Safe code using binding objects

findViewbyId의 경우 실 사용시 많은 버그들을 만들어 냅니다.
예를들어 layout에 없는 id로 view를 찾아오는경우 null을 반한하거나, crash를 만들기도 하고, 반환 타입에 safe하지 않기 때문에 잘못된 type으로 반환될때 exception을 만들어 내기도 합니다.
 
하지만 View binding을 사용할 경우 이 두가지의 문제점을 해결 할 수 있습니다.
 
  • Type-safe: view binding은 layout 내부에 정확한 view 타입을 찾아 mapping하므로 해당 view의 속성으로 접근할때 해당 view에 맞는 속성값을 노출시킵니다. 즉 예를들어 TextView라면 TextView의 속성에 맞는 properties만 노출됩니다.
  • Null-safe: 여러 설정이 존재하는 layout의 경우 해당 설정에 맞는 view만 찾아 냅니다. 그리고 그렇지 않은경우 @Nullable 속성으로 만듭니다.
 
View binding 이용시 java 클래스를 생성하지만, kotlin에서도 명확하게 동작하도록 해주기 때문에 (annotation을 명확하게 달아주므로) 두개의 언어 모두에서 사용할 수 있습니다.
 

What code does it generate?

위에서 계속되어 언급했지만, view binding은 모든 layout xml당 하나의 java class를 생성하고, 이름을 카멜 대소문자 형태로 만듭니다.
또한 Android studio는 xml 변경시 해당 xml에 대한 binding object만을 메모리에서 바로 갱신하도록 최적화 되어 있습니다.
따라서 xml이 변경시 editor에서 이 변경점을 바로 적용하기 위해 full rebuild를 할 필요가 없습니다.
 
 
public final class ActivityAwesomeBinding implements ViewBinding {   @NonNull   private final ConstraintLayout rootView;   @NonNull   public final Button button;   @NonNull   public final TextView subtext;   @NonNull   public final TextView title; 

 

위에서 예제로 들었던 XML은 view bidning을 통해 위와 같은 코드로 생성됩니다.

view binding은 View의 정확한 타입으로 view의 연결점을 제공하며, view의 id명이 변수명이 됩니다.

또한 getRoot를 통해 (kotlin은 그냥 root 겠죠?) root view를 얻어올 수 있습니다.

모든 속성값은 (멤버변수들은) @NonNull 또는 @Nullable annotation을 갖기 때문에 Kotlin로 변환되어 사용될때도 명확하게 null 여부를 알수 있습니다.
 

 

private ActivityAwesomeBinding(@NonNull ConstraintLayout rootView, @NonNull Button button,       @NonNull TextView subtext, @NonNull TextView title) { … }      @NonNull   public static ActivityAwesomeBinding inflate(@NonNull LayoutInflater inflater) {     /* Edited: removed call to overload inflate(inflater, parent, attachToParent) */     View root = inflater.inflate(R.layout.activity_awesome, null, false);     return bind(root);   } 

 

사실 내부적으로 보면 Binding class 내부에 존재하는 inflate method가 내부적으로 view를 infllate 시킵니다.

위 예제에서의 inflate method는 parent값은 null, attachToParent 값은 false로 고정되어 있지만 이 값들을 직접 넣어줄수있는 inflate method도 존재합니다.

 

그리고 마지막에 보면 bind 함수가 존재합니다.

이 bind 함수는 내부적으로 findViewById를 수행합니다.

 

 

 

@NonNull   public static ActivityAwesomeBinding bind(@NonNull View rootView) {     /* Edit: Simplified code – the real generated code is an optimized version */     Button button = rootView.findViewById(R.id.button);     TextView subtext = rootView.findViewById(R.id.subtext);     TextView title = rootView.findViewById(R.id.title);     if (button != null && subtext != null && title != null) {       return new ActivityAwesomeBinding((ConstraintLayout) rootView, button, subtext, title);     }     throw new NullPointerException("Missing required view […]");   }

compiler가 XML으로부터 직접적으로 타입을 check하고, 잠재적인 nullability를 확인 하기 때문에 안전하게 findViewById를 호출할 수 있습니다.

(실제로 bind의 코드는 훨씬 깁니다.)

 

정리하면  각 binding object에는 세개의 static function이 존재합니다.

  • inflate(inflater) - Activity의 onCreate에서 사용하며,  parent view로 넘길게 없을 경우 사용합니다.
  • inflate(inflater, parent, attachToParent) - Fragment나 RecyclerView의 Adapter(또는 ViewHolder) 처럼 parent ViewGroup를 넘겨야 하는경우 사용합니다.
  • bind(rootView) - 이미 view를 inflate 한상태에서 findViewById를 피하고 싶거나, 기존 코드를 refactoring할때 유용하게 사용할 수 있습니다.

 

What about included layouts

만약 layout이 include 로 다른 view를 include 하고 있다면 아래와 같이 binding 됩니다.
 
원본 view
<!-- activity_awesome.xml --> <androidx.constraintlayout.widget.ConstraintLayout>     <include android:id="@+id/includes" layout="@layout/included_buttons" </androidx.constraintlayout.widget.ConstraintLayout>    <!-- included_buttons.xml --> <androidx.constraintlayout.widget.ConstraintLayout>     <Button android:id="@+id/include_me" /> </androidx.constraintlayout.widget.ConstraintLayout> 

 

위와 같은 경우 binding object는 아래와 같이 생성됩니다.

 

public final class ActivityAwesomeBinding implements ViewBinding {   ...    @NonNull   public final IncludedButtonsBinding includes; 

 

여기서 주위해야 할 점은 반드시 <include> tag가 id를 가져야 한다는 겁니다.

그래야만 해당 id 명으로 속성이 생성됩니다.

아마도 IncludeButtonsBinding 안에 button에 연결 가능한 includeMe 속성이 있겠죠?

 

Using view binding and data binding

view binding은 findViewById를 대체하기 위한 방법으로만 사용됩니다.
따라서 기존에 제공되는 data binding과 View binding을 같은 모듈에서 동시에 사용할수도 있습니다.
(view binding은 data binding을 사용하지 않고 light한 사용을 위한 요청으로 인하여 개발되었다고 하네요~)
 

View binding and Kotlin synthetics or ButterKnife

Kotlin과 butterknife 대신 view binding을 쓸수 있느냐에 대한 질문을 많이 받는다고 합니다.
아무래도 좀더 안전하고, 정확하게 view를 찾아내는 view binding을 사용하는게 더 좋을거라고 하네요~
 

 

반응형