[Android] ViewBinding이란?

WonseokOh·2022년 5월 6일
0

Android

목록 보기
10/16

ViewBinding

  Activity, Fragment의 UI를 변경하기 위해서는 XML 파일을 인플레이션 후 findViewById를 통해 각 뷰를 참조합니다. 간단한 UI일 경우에는 크게 상관없지만 복잡한 UI일수록 보일러 플레이트 코드를 양산하게 됩니다. 이를 방지하고 쉽게 View와 상호작용 하기 위해서 나온 것이 DataBinding과 ViewBinding입니다. 이번 글에서는 ViewBinding에 대해서만 먼저 설명하도록 하겠습니다.


ViewBinidng 설정

  View Binding은 모듈별로 설정하기에 build.gradle(Module) 에 아래와 같이 설정해야 합니다.

android {
        ...
        viewBinding {
            enabled = true
        }
    }

모듈에서 설정된 View Binding은 모듈 내에 있는 모든 레이아웃 파일들에 해당하는 바인딩 클래스를 생성합니다. 또한, 바인딩 클래스 내에 레이아웃에 포함된 뷰들에 대한 참조가 각각 정의되어 있습니다.


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

모듈 내의 모든 레이아웃 파일들을 자동으로 바인딩 클래스를 생성되는 것을 무시하기 위해서는 다음과 같이 루트뷰에 tools:viewBindingIgnore = "true" 를 추가합니다.


ViewBinding 클래스 생성

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/searchEditText"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:inputType="text"
        android:lines="1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/bookRecyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/searchEditText" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/historyRecyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/white"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/searchEditText" />

</androidx.constraintlayout.widget.ConstraintLayout>

  다음과 같이 레이아웃 파일이 있다고 가정합니다. 루트 뷰는 ConstraintLayout이고 내부에 EditText 1개와 RecyclerView가 2개 존재합니다. 위에서 언급했듯이 View Binding이 설정되어 있으면 레이아웃에 상응하는 바인딩 클래스를 생성합니다. 바인딩 클래스의 이름은 XML 파일의 이름으로 Camel Case으로 변환하고 "Binding" 을 추가하여 생성합니다.


실제 바인딩 클래스를 보고 싶다면 안드로이드 스튜디오 Android 뷰에서 Project로 변경 후 app > build > ... > databinding 폴더 아래에 생성되는 것을 확인하실 수 있습니다.


Activity에서 View Binding 사용

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

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

  레이아웃 파일의 이름이 acitivity_main.xml 이라서 바인딩 클래스가 ActivityMainBinding으로 생성되었습니다. ActivityMainBinding의 Static 메소드인 inflate 함수를 호출하여 Binding 클래스를 생성합니다. 이후 setContentView에 루트 뷰를 전달하여 레이아웃의 화면을 나타냅니다.


// Generated by view binder compiler. Do not edit!
package ows.kotlinstudy.bookreview.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;
import ows.kotlinstudy.bookreview.R;

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final RecyclerView bookRecyclerView;

  @NonNull
  public final RecyclerView historyRecyclerView;

  @NonNull
  public final EditText searchEditText;

  private ActivityMainBinding(@NonNull ConstraintLayout rootView,
      @NonNull RecyclerView bookRecyclerView, @NonNull RecyclerView historyRecyclerView,
      @NonNull EditText searchEditText) {
    this.rootView = rootView;
    this.bookRecyclerView = bookRecyclerView;
    this.historyRecyclerView = historyRecyclerView;
    this.searchEditText = searchEditText;
  }

  @Override
  @NonNull
  public ConstraintLayout 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.bookRecyclerView;
      RecyclerView bookRecyclerView = rootView.findViewById(id);
      if (bookRecyclerView == null) {
        break missingId;
      }

      id = R.id.historyRecyclerView;
      RecyclerView historyRecyclerView = rootView.findViewById(id);
      if (historyRecyclerView == null) {
        break missingId;
      }

      id = R.id.searchEditText;
      EditText searchEditText = rootView.findViewById(id);
      if (searchEditText == null) {
        break missingId;
      }

      return new ActivityMainBinding((ConstraintLayout) rootView, bookRecyclerView,
          historyRecyclerView, searchEditText);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

  위의 코드는 실제로 생성된 ActivityMainBinding 클래스로 속성에 rootView 이외에도 rootView에 포함된 모든 뷰들에 대한 참조가 존재합니다. 내부적으로 ActivityMainBinding 클래스가 생성되는 흐름을 살펴보는 것도 좋습니다.

  1. inflate(LayoutInflater)
  2. inflate(LayoutInflater, ViewGroup, boolean)
  3. bind(View)

  Activity 같은 경우는 parent가 존재하지 않기에 inflate(LayoutInflater)가 호출되지만, Fragment 나 RecyclerView의 Item에 해당하는 View들은 inflate(LayoutInflater, ViewGroup, boolean)이 바로 호출될 수 있습니다.
그리고 인플레이션을 통해 rootView를 얻는다면 bind 메소드에 rootView를 대입하여 모든 뷰에 대한 참조를 findViewById를 통해 얻어오게 됩니다. 리턴 값으로 ActivityMainBinding을 반환하게 되고 참조한 뷰들까지 모두 세팅하여 마무리하게 됩니다. 이런 로직을 통해서 ActivityMainBinding.inflate 메소드만 호출하더라도 모든 뷰들을 바인딩 클래스로 참조를 할 수 있습니다.


Fragment View Binding 사용

  Activity에서는 View Binding을 사용할 때 큰 주의사항은 없지만 Fragment에서 View Binding을 사용할 때 주의할 점이 있습니다.

    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
    }

  onCreateView에서 View Binding 클래스를 생성하고 onDestroyView에서는 꼭 View Binding을 null 처리 해주어야 합니다. Fragment의 생명주기로 보면 onDestory 메소드가 onDestoryView 다음에 호출되기에 View보다 오래 지속된다고 볼 수 있습니다.

  만일 Fragment의 인스턴스는 유지하지만 Fragment View는 사용하지 않아 메모리 해제하는 경우도 있습니다. 하지만 Fragment View에 해당되는 Binding 클래스를 null 처리하지 않을 경우에 Fragment가 Binding 클래스를 강하게 참조하고 있어서 GC가 수집하지 않게 됩니다. 즉, Fragment의 onDestroyView까지 콜백 메소드가 호출되어서 View 메모리 해제를 예상하지만 실제로는 Fragment에서의 강한 참조로 인해 동작을 안하게 되어 메모리 릭을 발생시키게 됩니다. 이 부분이 이해가 가지 않는다면 Stronge Reference, Week Reference에 대해서 공부해보셔도 좋을 것 같습니다.


findViewById와의 차이점

Null 안전성

바인딩 클래스가 직접 뷰들을 참조하기에 잘못된 id로 인한 NullPointerException과 휴먼에러를 방지합니다.

Type 안정성

위의 장점과 마찬가지로 바인딩 클래스가 XML 파일에 정의된 뷰들을 가지고 참조를 하기에 잘못된 캐스팅과 같은 에러를 방지합니다.


DataBinding과의 차이점

더 빠른 컴파일

@BindingAdapter와 같은 어노테이션이 없어서 어느테이션 프로세서를 사용하지 않아 컴파일 시간이 짧습니다.

편리한 사용성

DataBinding은 XML 파일에 layout 태그를 추가해야만 바인딩 클래스를 생성하는데 반해 ViewBinding은 모든 모듈 내에 존재하는 레이아웃을 자동으로 생성하여 편리합니다.

레이아웃 표현식

DataBinding은 레이아웃 내에 이벤트를 설정하거나 값을 대입할 수 있어 선언형 프로그래밍이 가능합니다. 하지만 ViewBinding은 지원하지 않습니다.

양방향 데이터 결합

ViewBinding은 코드 내에서만 레이아웃을 참조했지만 DataBinding은 레이아웃에서 코드 내의 변수를 참조하여 값을 변경하거나 UI를 변경할 수 있어서 양방향으로 상호작용이 가능합니다.


참고

profile
"Effort never betrays"

0개의 댓글