View와 상호작용하는 코드를 쉽게 작성할 수 있게 도와주는 기능
기존에는 XML 파일의 View에 접근할 때 findViewById()
메소드를 사용했다. 그러나 이는 개발자들에게 상당한 불편함을 주었다.
Null Pointer Exception
이 발생한다.Class Cast Exception
이 발생한다.그래서 첫번째 대안으로 나왔던 것이 코틀린 익스텐션(Kotlin Extension)이었는데, View의 id로 직접 접근하는 방법이었다. 기존 findViewById
에 비해서는 상당히 편리했으나, 시간이 지나며 여러 문제점이 발견되기 시작했다.
이런 과정 속에서 코틀린 익스텐션 역시 deprecated 되며, ViewBinding 과 DataBinding이 탄생하게 되었다. DataBinding에 대해서는 다음 포스팅에서 알아보기로 하고, 오늘은 ViewBinding에 대해 자세히 알아보도록 하자.
자, 그러면 ViewBinding 을 직접 사용해보도록 하자.
android {
buildFeatures {
viewBinding = true
}
}
앱 수준의 그래들에서 ViewBinding을 활성화하면 모든 레이아웃 XML 파일의 Binding 클래스가 생성된다. 클래스의 이름은 해당 XML을 카멜 표기법으로 변환 후, 뒤에 Binding을 붙여 완성한다.
list_fragment.xml
-> ListFragmentBinding
hoya_activity.xml
-> HoyaActivityBinding
<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=".ListFragment">
<TextView
android:layout_width="wrap_content"
android:layout_height="50dp"
android:id="@+id/title"
android:text="Pcheduler"
android:fontFamily="@font/gowundodum"
android:textColor="@color/ft_white"
android:textSize="40sp"
android:layout_marginTop="15dp"
android:layout_marginLeft="20dp"
android:gravity="center"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
XML 구성은 다음과 같다. TextView
가 있는 매우 심플한 코드 구성이다.
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding // 뷰 바인딩
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityMainBinding.inflate(layoutInflater)
// inflate() 메소드 호출
val view = binding.root
// getRoot() 메소드 호출
setContentView(view)
// Root View 를 setContentView() 에 전달하여 화면상의 View로 만든다.
binding.title.setText("velog.io/@hoyaho/")
}
}
🤔 binding.root?
이는 Root View 를 참조한다는 의미인데, Root View는 레이아웃에서 가장 바깥쪽의 View Container 이다. 즉, 호출 시 XML에 있는 ConstraintLayout의 Root View를 반환한다는 의미이다.
이 과정을 거침으로서, 해당 레이아웃에 있는 id만 사용할 수 있게 되어 다른 레이아웃에 있는 동일한 이름의 id와 헷갈릴 일이 없어지는 것이다.
class ListFragment : Fragment() {
private var _binding : FragmentListBinding? = null // 뷰 바인딩
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentListBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.title.text = "https://velog.io/@hoyaho"
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // 메모리 누수 방지를 위해 null 처리
}
}
코드를 보면 알겠지만, 프래그먼트에서 View Binding 사용이 액티비티와 다른 점을 확인할 수 있다. 이유는, 프래그먼트와 액티비티의 생명주기가 다르기 때문이다.
기본적으로 프래그먼트에서는 onDestroyView()
메소드가 호출될 때 프래그먼트 객체 자체가 사라지지 않고 메모리에 남아있는데, 이로 인해 메모리 누수가 발생할 수 있다.
고로, onDestroyView()
메소드에서 직접 binding 객체를 null 로 만들어 가비지 컬렉터에서 수집해가도록, 즉 사라지게 해야 메모리 누수를 방지할 수 있는 것이다.
또한, binding 변수를 NULL able로 선언 후 다시 한번 NON-NULL 타입으로 포장하는 것을 확인할 수 있는데, NULL able로 사용하면 코드를 작성할 때마다 ?.
를 붙여야하는 불편함이 있어 편의를 위해 포장하는 것이다. 다만, 이런 경우 NULL Safe 가 보장되지 않기 때문에 유의하며 코드를 작성해야 한다.
참고로, _binding
에서 _
은 private 한 변수를 사용할 때 관례적으로 붙이는 특수기호이니 알아두도록 하자.
class ListAdapter(
val context: Context,
var list: List<TaskEntity>
) : RecyclerView.Adapter<ListAdapter.MainViewHolder>() {
override fun getItemCount(): Int {
return list.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
// val itemView = LayoutInflater.from(context).inflate(R.layout.item_list, parent, false)
// return MainViewHolder(itemView)
val binding = ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MainViewHolder(binding)
// binding access
}
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val task = list[position]
holder.taskText.text = task.content
}
// binding의 root view 형태로 지정한다.
inner class MainViewHolder(private val binding : ItemListBinding) : RecyclerView.ViewHolder(binding.root) {
var taskText = binding.listEdt
var taskImage = binding.listImg
// 생성자에서 val 로 binding 을 받았기 때문에 Holder 전역에서 사용이 가능하다.
}
// inner class MainViewHolder(val view : View) : RecyclerView.ViewHolder(view)
}
세부적인 사항은 주석을 확인하면 되고, 큰 틀로 보았을 때 View를 생성자로 사용하던 것을 Binding으로 교체된 것을 중점적으로 보면 이해하기 쉬울 것이다.
참고 및 출처
ViewBinding을 프레그먼트에서 사용할 때 Memory Leak 방지하기
안드로이드 공식 문서
뷰 바인딩(View Binding) in Activity, Fragment