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