본문 바로가기
Android

[Android] ViewBinding

by Glion 2023. 10. 30.
반응형

Android 앱을 만들때, 제일 처음에 " findViewById(R.id.***) " 을 이용하여 xml에 작성한 View를 가져와

특정한 작업을 하는 방법을 배운다.

이런 작업에는 View를 가져와 저장할 변수, 해당 변수에 findViewById 를 사용하여 가져오는 작업이

필요하다.

그러나 프로젝트의 규모가 커질수록, findViewById 코드의 양은 점점 많아질 것이고, onCreate의 길이도

길어질 뿐더러 findViewById로 가져온 View 를 저장할 변수 또한 매우 많아질 것이다.

또한 존재하지 않는 id를 가져오도록 하여 NullPointerException의 위험 또한 존재한다.

 

이러한 findViewById의 단점이 나타나자, 이를 대체할 ButterKnife 라던지, 지금은 deprecated 된 Kotlin Synthetic Properties 가 있지만 현재 가장 추천되는 방식은 ViewBinding 혹은 DataBinding 이다.

 

이번 글에서는 ViewBinding에 대해서만 알아본다.

 

ViewBinding?

findViewById 를 대체할 수 있는 방법으로, 현재 구글 공식문서에도 나와있으며 가장 추천되는 방식이다.

 

사용방법(Activity)

app 수준 build.gradle 을 열고, Android { ... } 안에 다음 코드를 추가해준다.

buildFeatures{
	viewBinding = true
}

 

다음으로 레이아웃을 작성한다. 간단한 예시로 다음과 같은 레이아웃을 작성하였다.

텍스트뷰를 클릭하면 내용이 바뀌고, 버튼을 클릭하면 텍스트뷰가 사라지는 간단한 기능을 가질 예정이다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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"
    tools:context=".viewbinding.ExViewBindingActivity"
    android:orientation="vertical">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv_ex"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="클릭하면 바뀝니다."/>

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_ex"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="클릭하면 텍스트뷰가 사라집니다."/>

</androidx.appcompat.widget.LinearLayoutCompat>

 

Activity로 돌아와서, mBinding 변수를 생성, 초기화해준다.

build.gradle에서 viewBinding을 추가해주었기 때문에 작성하는 layout은 자동으로 뷰 결합 클래스로 만들어진다.

만약, 뷰 결합 클래스를 만들고 싶지 않은 layout이라면, 최상단에 " tools:viewBindingIgnore="true" " 해주게 되면,

뷰 결합 클래스가 만들어지지 않고, 아래 설명하는 방법을 사용할 수 없게 된다.

 

뷰 결합 클래스는 _제외한 CamelCase 형식의 레이아웃 이름 + Binding 으로 자동 생성된다.

_제외한 CamelCase 형식의 레이아웃 이름 + Binding

class ExViewBindingActivity : AppCompatActivity() {
    private lateinit var mBinding : ActivityExViewBindingBinding 
    // 자동생성된 뷰 결합 클래스를 저장하는 변수이다. 이 변수를 inflate 하여 화면을 띄운다.
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // getLayoutInflater 대신 kotlin 의 get/set을 대신하는 layoutInflater로 사용이 가능하다.
        mBinding = ActivityExViewBindingBinding.inflate(getLayoutInflater())
        // inflate 하여 xml 레이아웃파일을 코드화 하였으니, 이를 이용해 setContentView()의 인자에 넣어주고 화면을 띄운다.
        setContentView(mBinding.root)
    }
}

이제 각 행동을 정의해보자.

레이아웃에 정의된 view를 사용할 때는 id를 사용하는데, view 의 id에서 _가 제거된 이름을 사용한다.

mBinding. 하고 자동완성을 키면 레이아웃에서 정의한 id 값이 다음과 같이 나오게 된다.

tv_ex 와 btn_ex 로 정의했던 id가 tvEx, btnEx로 나타난다.

클릭했을때의 행동을 정의한다.

// onCreate 내부에 작성함        
        mBinding.tvEx.apply{
            setOnClickListener {
                mBinding.tvEx.text = "변경된 텍스트"
            }
        }

        mBinding.btnEx.apply{
            setOnClickListener {
                mBinding.tvEx.visibility = View.INVISIBLE
            }
        }

기존 findViewById를 사용하는 방식이였다면, AppCompatTextView 타입의 변수 tvEx 를 만들고, findViewById(R.id.tv_ex) 로 초기화해주고, tvEx.apply{ setOnClickListener{ ... } } 해주어야 하는데 viewBinding을 사용하면 짧고 간편하게 사용할 수 있다.

 

실행해보면, 잘 동작하는 것을 확인 할 수 있다.

 

사용방법(Fragment)

Fragment 에서 사용할 때도 동일한 과정을 거친다.

앞에서, 뷰 결합 클래스가 자동생성될때 _를 제외한 CamelCase 형식의 레이아웃명 + Binding 으로 자동생성 된다고

하였다.

Fragment의 뷰 결합 클래스는도 마찬가지이다.

레이아웃의 이름이 fragment_ex_view_binding.xml 이라면 FragmentExViewBindingBinding 이 된다.

 

Fragment 클래스의 전역 변수에 FragmentExViewBindingBinding 타입의 변수를 생성하고 onCreateView 에서 mBinding을 초기화해준다.

그 뒤 return inflater.inflate(R.layout.***, container, false) 부분 대신 return mBinding.root 해준다.

 

말로 설명하니 장황하지만, 코드로 보면 다음과 같다.

class ExViewBindingFragment : Fragment() {

    private lateinit var mBinding: FragmentExViewBindingBinding
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mBinding = FragmentExViewBindingBinding.inflate(inflater) // onCreateView의 LayoutInflater 타입의 매개변수 inflater 이용.

        return mBinding.root
    }
}

여기서 Fragment 내의 view에 대한 동작을 정의하고 싶다면, 전역변수로 생성한 mBinding을 이용하여 Activity 에서와

마찬가지로 정의해 줄 수 있다.

 

앞의 액티비티에서 버튼을 클릭하면 fragment가 나오게 한다면,

supportFragmentManager.beginTransaction 에서 (Fragment에 대해서는 추후 자세히 다룰 것) container의 Id 가 들어가는 부분에 기존처럼 R.id.fragment_container_id 해도 되지만, mBinding.[지정한 FragmentContainer Id].id 를 해주어도 동일하다.

// supportFragmentManager.beginTransaction().replace(R.id.frag_view, ExViewBindingFragment()).commit()
supportFragmentManager.beginTransaction().replace(mBinding.fragContainer.id, ExViewBindingFragment()).commit()
                
// 두개가 동일하다

 

단, Fragment에서 ViewBinding을 사용할 때 주의해야할 점이 한가지가 있다.

Fragment의 생명주기가 Fragment View 의 생명주기보다 길고, onDestroyView로 Fragment View 가 종료되어도 Fragment는 그 이후에 종료되게 된다.

또한 ragment 는 재사용을 위해서 onDestroy가 호출됬을때 Fragment에 대한 참조는 사라지지만, 내부적으로는 View 들은 재사용을 위해서 보관된다. 

View 가 보관되어있고, onDestroyView가 호출되어도 Fragment는 아직 종료되지 않은 상태이기에 mBinidng 또한 남아있다. 메모리 누수의 원인이 될 수 있다는 말이다.

따라서 onDestroyView에서 mBinding을 null로 초기화해주어야 메모리 누수를 막을 수 있다.

(그렇다면 lateinit 했던 mBinding을 nullable 한 변수로 변경해주어야겠다)

 

더보기

ExViewBindingActivity

package com.example.practice_and.viewbinding

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import com.example.practice_and.R
import com.example.practice_and.databinding.ActivityExViewBindingBinding

class ExViewBindingActivity : AppCompatActivity() {
    private lateinit var mBinding : ActivityExViewBindingBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityExViewBindingBinding.inflate(layoutInflater)
        setContentView(mBinding.root)

        mBinding.tvEx.apply{
            setOnClickListener {
                mBinding.tvEx.text = "변경된 텍스트"
            }
        }

        mBinding.btnEx.apply{
            setOnClickListener {
                // open Fragment
//                supportFragmentManager.beginTransaction().replace(R.id.frag_view, ExViewBindingFragment()).commit()
                supportFragmentManager.beginTransaction().replace(mBinding.fragContainer.id, ExViewBindingFragment()).commit()
            }
        }
    }
}

 activity_ex_view_binding.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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"
    tools:context=".viewbinding.ExViewBindingActivity"
    android:orientation="vertical"
    tools:viewBindingIgnore="">

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv_ex"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="클릭하면 바뀝니다."/>

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btn_ex"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="클릭하면 프래그먼트가 열립니다."/>

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/frag_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.appcompat.widget.LinearLayoutCompat>

ExViewBindingFragment

package com.example.practice_and.viewbinding

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.example.practice_and.R
import com.example.practice_and.databinding.FragmentExViewBindingBinding

class ExViewBindingFragment : Fragment() {

    private lateinit var mBinding: FragmentExViewBindingBinding
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mBinding = FragmentExViewBindingBinding.inflate(inflater) // onCreateView의 LayoutInflater 타입의 매개변수 inflater 이용.

        return mBinding.root
    }
}

fragment_ex_view_binding

<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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"
    tools:context=".viewbinding.ExViewBindingFragment"
    android:gravity="center"
    android:background="@color/gray">

    <androidx.appcompat.widget.AppCompatTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ViewBinding Fragment" />

</androidx.appcompat.widget.LinearLayoutCompat>

 


참고자료

https://developer.android.com/topic/libraries/view-binding#findviewbyid

 

뷰 결합  |  Android 개발자  |  Android Developers

뷰 결합 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 뷰 결합 기능을 사용하면 뷰와 상호작용하는 코드를 쉽게 작성할 수 있습니다. 모듈에서 사용 설정

developer.android.com

https://yoon-dailylife.tistory.com/57

 

Android) Fragment에서 View Binding 문제점, 제대로 사용하기

View Binding을 모르시는 분들은 이전 글에서 확인 부탁드립니다. Problems in ViewBinding View Binding in Fragment private var _binding: ResultProfileBinding? = null // This property is only valid between onCreateView and // onDestroyView.

yoon-dailylife.tistory.com

https://gift123.tistory.com/58

 

안드로이드 개발 (29) Fragment에서 ViewBinding 사용 시 주의할 점

1. ViewBinding ViewBinding 은 xml를 자동으로 바인딩 클래스로 생성해서 xml의 View를 안전하게 사용할 수 있습니다. kotlin extension deprecated 이 되고나서 요즘은 ViewBinding, DataBinding을 위주로 사용하는 추세

gift123.tistory.com

 

반응형