findViewById 대신 View Binding 사용하기

이번 포스팅에서는 View Binding을 사용하는 법에 대해 알아보도록 하겠습니다.

Kotlin Android Extensions의 지원 중단

안드로이드 스튜디오에서는 뷰를 만들고 그 뷰를 불러오기 위해서 findViewById를 사용합니다. 이 때 kotlin-android-extensions 플러그인을 사용하면 이 함수의 사용을 생략하고 간편하게 코드를 작성할 수 있습니다.

문제는 서로 다른 xml파일을 만들었을 때 Id를 동일하게 사용할 수 있기 때문에 코드에서 헷갈릴 수 있다는 문제가 있습니다. 그래서 구글에서는 안드로이드 스튜디오 4.1부터 kotlin-android-extensions의 지원을 중단하고 View Binding을 사용하도록 안내하고 있습니다.

View Binding의 구조

View Binding을 프로젝트에서 사용하도록 설정하면 각 레이아웃 xml 파일에 대해 ViewBinding 클래스를 상속받는 개별 binding class가 자동으로 생성됩니다. 이 때 클래스 이름은 레이아웃 파일 이름을 파스칼 표기법으로 변환하고 Binding 접미사를 추가합니다.

그리고나서 onCreate 안에서 View Binding 클래스에 인스턴스를 생성하면 뷰의 Id를 인스턴스의 프로퍼티로 사용할 수 있게 됩니다.

View Binding의 장점

구글에서 설명하는 View Binding의 장점은 두가지가 있습니다. 하나는 Null-safe로, 서로 다른 레이아웃에 대해 같은 ID를 가진 뷰를 정확히 구분할 수 있게 됩니다. 만약 Id를 참조할 수 없을 경우 @Nullable로 만들어 아예 사용할 수 없게 막습니다. 두번째는 Type-safe로, findViewById를 사용할 경우 뷰에 잘못된 타입을 지정할 수 있다는 문제가 있는데, View Binding에서는 그런 문제가 발생하지 않는다는 특징이 있습니다.

findViewById 사용상의 문제

안드로이드 스튜디오의 Create new project에서 Basic Activity를 만들어서 View Binding 환경을 구현해 보겠습니다. 우선은 findViewById를 사용할 때 어떤 문제가 발생하는지 보여드리겠습니다.

kotlin-android-extensions 플러그인을 적용한 뒤, fragment_first.xml, fragment_second.xml의 텍스트뷰 Id를 동일하게 지정해줍니다.

이 상태에서 FirstFragment.kt에서 텍스트뷰를 사용하면 연결된 xml이 자동으로 임포트 되는데, 코드만 봐서는 이게 fragment_first.xml에서 온 코드인지 fragment_second.xml에서 온 것인지 알 수가 없습니다.

참조한 뷰가 한개일 때는 import 파트를 보면 출처를 알 수 있지만 사용하는 뷰가 많아지면 어떤 뷰가 어떤 import를 참조한 것인지 한눈에 파악하는 것이 매우 어려워지게 되는 문제가 있습니다.

Activity에 View Binding 적용

다음과 같이 gradle에서 View Binding을 활성화하면 모든 레이아웃 xml 파일에 대해 바인딩클래스가 생성됩니다.

1
2
3
android {
    buildFeatures.viewBinding true
}

우선은 메인액티비티에 적용해보겠습니다. 자동으로 생성된 ActivityMainBinding 타입의 binding 전역변수를 준비하고 onCreate 에서 inflate시킵니다.

그러면 View Binding을 사용할 수 있게 되므로 기존에 R.layout.activity_main를 전달하던 setContentViewbinding.root를 전달합니다. 또 findViewById를 사용하던 setSupportActionBar에 전달하는 값도 binding.toolbar로 변경합니다. 플로팅버튼은 binding.fab로 사용할 수 있습니다.

참고로 onCreateOptionsMenu, onOptionsItemSelected 안의 내용은 findViewById()가 아니라 findItem()이라는 메소드를 사용하는것이기 때문에 View Binding으로 대체를 할 수 없습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
//        setSupportActionBar(findViewById(R.id.toolbar))
//
//        findViewById<FloatingActionButton>(R.id.fab).setOnClickListener { view ->
//            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
//                    .setAction("Action", null).show()
//        }
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.toolbar)

        binding.fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG).setAction("Action", null).show()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }
}

Fragment에 View Binding 적용

다음은 Fragment에 View Binding을 적용해 보겠습니다. Fragment에서 사용할 때는 _bindingbinding 변수 두개를 만들어줍니다. _binding은 Fragment를 사용하지 않을 때 자원을 반환할 수 있도록 Nullable 형태로 준비합니다. 그리고 실제로 사용하는 binding_binding이 초기화 되었을 때 get()으로 값을 가져오도록 합니다.

Fragment의 라이프사이클을 고려해 _bindingonCreateView 안에서 초기화하면 binding이 자동으로 생성됩니다. 그러면 binding.root를 뷰로 반환하여주고, onViewCreated에서는 binding의 프로퍼티로써 텍스트뷰와 버튼을 사용하면 됩니다.

마지막으로 onDestroyView에서는 Fragment가 어떠한 이유로 파괴될 때 _binding을 null로 만들어서 자원을 반환하도록 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class FirstFragment : Fragment() {
    private var _binding : FragmentFirstBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
//        return inflater.inflate(R.layout.fragment_first, container, false)
        _binding = FragmentFirstBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

//        view.findViewById<Button>(R.id.button_first).setOnClickListener {
//            findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
//        }
        binding.textview.text = "first fragment text"
        binding.buttonFirst.setOnClickListener {
            findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

SecondFragment의 내용은 FirstFragment와 동일하므로 생략하겠습니다. 이렇게 해서 프로젝트의 액티비티와 Fragement에 View Binding을 적용하는 법에 대해 알아보았습니다.

Built with Hugo
Theme Stack designed by Jimmy