Android View Binding (뷰 바인딩)

목표

View Binding을 적용한다

2022.01.24 - [Android] - Android Fragment 설정하기

2022.01.24 - [Android] - Android Fragment Navigation with Action

2022.01.25 - [Android] - Android Fragment with Arguments

앞서 진행한 posts의 연속입니다. 마지막 포스트에있는 아래 소스 다운받고 보시면 더 편하실거에요.

https://github.com/theyoung/fragmentsetup/tree/2de0436018dde923c210601237c121df6909aa4c

 

findViewById의 단점

앞서 만든 TestFragment.kt 파일을 확인해 보자

class TestFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_test, container, false)
        view.findViewById<Button>(R.id.button).setOnClickListener {
            val str = view.findViewById<EditText>(R.id.editTextTextPersonName).text.toString() ?: "hello"
            val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
            view.findNavController().navigate(action)
        }
        return view
    }

}

R.layout.fragment_test라고 하는 fragment에서 root view를 갖어 왔다.

이 root view에서 onClick Event를 listen할 button을 ID로 갖어와서 Button으로 Class Casting후 사용하게 된다.

여기서 크게 2가지 문제점이 발생하게 된다.

  • Not Null Safe : 만약 R.id.button이라는 ID가 없다면 setOnclickListener가 Null Point Exception이 발생하게 된다
view.findViewById<Button>(R.id.button).setOnClickListener{....}
  • Not Type Safe : Casting Class가 Button이 아니라면? 개발자 실수에 의해 오류가 발생하게 됩니다.
view.findViewById<Button>

 

viewBinding 설정하기

app 하위에 있는 build.gradle에 buildfeatures를 추가해 줍니다.

android {
...

    buildFeatures {
        viewBinding true
    }
}

build.gradle을 수정 후 sync > clean Project > RebuildProject를 진행해 줍니다.

이와 같이 3가지를 진행하고 나면 Project Perspective에서 Activity 및 Fragment binding이 XML 명과 비슷하게 build 된것을 확인 할 수 있습니다.

이것이 확인이 안되면 view bindind이 안된다고 생각하시면 됩니다.

 

 

Fragment에 Binding 설정하기

TestFragment.kt에 있는 binding 설정을 변경해 보자.

앞에서 보았던 원본 소스는 아래와 같습니다.

class TestFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        val view = inflater.inflate(R.layout.fragment_test, container, false)
        view.findViewById<Button>(R.id.button).setOnClickListener {
            val str = view.findViewById<EditText>(R.id.editTextTextPersonName).text.toString() ?: "hello"
            val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
            view.findNavController().navigate(action)
        }
        return view
    }

}

이 소스를 preBuilt된 binding소스를 이용해서 변경해 볼 예정입니다.

우선 TestFragement와 연결된 xml인 R.layout.fragment_test에서 파생된 build 파일인 FragmentTestBinding을 연결 시켜야합니다.

class TestFragment : Fragment() {
    var _binding: FragmentTestBinding? = null
    val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? { ....

우선

var _binding: FragmentTestBinding? = null

이와같이 작성한 이유는 이후에 onDestroyView를 통해서 null처리가 필요하기 때문이다.

그런데 문제는 binding하는 코드 자체에 null일경우 null exception이 가능한 코드가 될 가능성이 있다.

        _binding = FragmentTestBinding.inflate(inflater,container,false)
        _binding.button.setOnClickListener {

위에 코드를 보면 _binding에서 바로 button이라고 하는 view 객체를 접근하는 것을 볼 수 있다.

앞서 null able로 _binding이 선언되었기 때문에 잠재적으로 null exception을 발생할 여지가 있다.

그래서 임시 방편으로 null check를 넣어 줄순 있지만, 좋지 않은 방법이다.

        _binding = FragmentTestBinding.inflate(inflater,container,false)
        _binding?.button?.setOnClickListener {

그래서

    var _binding: FragmentTestBinding? = null
    val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        _binding = FragmentTestBinding.inflate(inflater,container,false)
        binding.button.setOnClickListener {
            val str = binding.editTextTextPersonName.text.toString() ?: "hello"
            val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
            binding.root.findNavController().navigate(action)
        }

        return binding.root
    }

상기와 같이 binding get() 을 사용해서 null이 없음을 보여주게 됩니다.

        binding.button.setOnClickListener {
            val str = binding.editTextTextPersonName.text.toString() ?: "hello"
            val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
            binding.root.findNavController().navigate(action)

실제 binding한 코드를 보면 findviewbyid와는 다르게 button을 직접 access하는 것을 볼수 있습니다. 이를 통해서 특정 view의 객체에 더빠른 접근과 typecating으로 부터 오는 잠재적 오류를 사전에 막을 수 있습니다.

가장 중요한 부분이 남았는데요. 바로 onDestroyView의 사용입니다.

해당 시점에 _binding = null 을 사용해서 객체를 해제해주는 것을 볼 수 있습니다.

    override fun onDestroyView() {
        super.onDestroyView()
        Log.d(this.javaClass.name,"finish view")
        _binding = null
    }

fragment는 activity의 lifecycle을 활용하지만 정확히는 activity의 한파트로써 독립적인 라이프 사이클을 갖게 됩니다.

https://www.geeksforgeeks.org/difference-between-a-fragment-and-an-activity-in-android/

그래서 activity와 다르게 onDestroyView와 onDestroy 둘다 사용이 가능합니다.

하지만 onDestroy는 Class finish이후 불특정한 시점에 불리게 됨으로 Leak을 발생시킬 가능성이 있습니다.

그래서 화면에서 View가 사라지는 순간 _binding을 null화 함으로써 GC시 clear가능상 상태로 만들어 줘야 합니다.

만약에 _binding = null을 처리해주지 않으면 아래와 같이 Leak의 대상이 되는 것을 확인 할 수 있습니다.

위의 내용가지 완료 되면 아래와 같이 정상 작동 되는 것을 확인 할 수 있습니다.

전제 코드는 아래와 같습니다.

package com.example.fragmentsetup

import android.os.Bundle
import android.util.Log
import android.util.Log.*
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import androidx.navigation.findNavController
import com.example.fragmentsetup.databinding.FragmentTestBinding
import java.util.logging.Level.INFO
import java.util.logging.Logger


/**
 * A simple [Fragment] subclass.
 * Use the [TestFragment.newInstance] factory method to
 * create an instance of this fragment.
 */
class TestFragment : Fragment() {
    var _binding: FragmentTestBinding? = null
    val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        _binding = FragmentTestBinding.inflate(inflater,container,false)
        binding.button.setOnClickListener {
            val str = binding.editTextTextPersonName.text.toString() ?: "hello"
            val action = TestFragmentDirections.actionTestFragmentToSecondFragment(str)
            binding.root.findNavController().navigate(action)
        }

        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        Log.d(this.javaClass.name,"finish view")
        _binding = null
    }

}

기능상으로는 별차이 없겠죠?

소스는 아래에 있습니다.

https://github.com/theyoung/fragmentsetup/tree/63ade4463beda013c96a7a85f7b48ce757f4ebc5

 

GitHub - theyoung/fragmentsetup

Contribute to theyoung/fragmentsetup development by creating an account on GitHub.

github.com

 

ViewModel을 활용해서 ListView를 표현해 보겠습니다. 

2022.02.05 - [Android] - Android ViewModel & ListView 사용하기

728x90
반응형