(안드로이드) NullPointerException – ViewTreeObserver.dispatchOnGlobalLayout

LoginFragment.kt에서 NullPointerException 발생


  • 이 오류가 처음 발생했을 때 첫 번째 줄에 getBinding이 표시되었습니다. 바인딩되어 있습니까? 제 생각에는.
  • 그러나 나중에 ViewTreeObserver 및 OnGlobalLayout의 문제로 인해 발생한 것으로 밝혀졌습니다.
    • 오류 코드를 읽을 때는 전반부만 보지 말고 후반부도 보는 습관을 길러야 합니다!

로그인 Fragment.kt

class LoginFragment : Fragment() {

    private var _binding: FragmentLoginBinding? = null
    private val binding get() = _binding!!
    private var waitTime = 0L

    @SuppressLint("ClickableViewAccessibility")
    override fun onCreateView(

        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        _binding = FragmentLoginBinding.inflate(inflater, container, false)

        initView()

				...

        return binding.root
}

	...

    @SuppressLint("ClickableViewAccessibility")
    private fun initView() {

        val showDefaultHeight = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 150f,
					resources.displayMetrics
					).toInt()

        val hideDefaultHeight = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 150f,
					resources.displayMetrics
					).toInt()


		binding.loginLayout.viewTreeObserver.addOnGlobalLayoutListener(object :
		            ViewTreeObserver.OnGlobalLayoutListener {
		            override fun onGlobalLayout() {
		
		                val viewHeight: Int = binding.loginLayout.height
		                val rootHeight: Int = binding.loginLayout.rootView.height
		                val diff = rootHeight - viewHeight
		
		                if (diff > showDefaultHeight) {
		                    setSNSLoginUIVisibility(binding, View.GONE)
		                } else if (diff < hideDefaultHeight) {
		                    Handler(Looper.getMainLooper()).postDelayed({
		                        setSNSLoginUIVisibility(
		                            binding,
		                            View.VISIBLE
		                        )
		                    }, 10)
		                }

                binding.loginLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
            }
        })

...

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

}

분석하다

  • 우선 OnGlobalLayout의 로직은 rootView의 높이를 계산하고 소프트 키보드의 적절한 위치에 엔터 버튼을 생성하는 것입니다.
  • ViewTreeObserver 사용 방법을 검색하면 항상 설명이 있습니다.다음과 같이
    • ViewTreeObserver를 사용한 후에는 removeOnGlobalLayoutListener()를 구현해야 합니다.
      • 그렇지 않으면 Observer가 무한한 수의 레이아웃 변경 감지를 요청하기 때문에 메모리 누수가 발생합니다.
        • 레이아웃 변경만 감지하려는 경우 addOnGlobalLayoutListener에서 removeOnGlobalLayoutListener를 직접 구현할 수 있습니다.
  • 하지만 제 경우에는 지속적으로 변화를 감지하고 다음 화면으로 이동할 때 멈추기를 원합니다.
    • 한 걸음 더 나아가 원래 화면으로 돌아왔을 때 옵저버가 다시 작동하면 좋겠다는 생각을 했습니다.
      • 이 경우 프래그먼트의 onStop() 수명 주기에서 removeOnGlobalLayoutListener를 구현할 수 있습니다.
      • onStop()은 프래그먼트가 화면에서 사라질 때 호출되는 메서드입니다.

내가 뭘 하고 싶어

사용자가 ID 창이나 PW 창을 클릭할 때마다 Observer가 레이아웃의 크기를 감지하여 키보드 뒤의 모든 요소를 ​​사라지게 하거나 다시 나타나게 하는 기능을 구현하고 싶습니다.

즉, 레이아웃 변경을 지속적으로 감지할 수 있기를 원합니다.

또한 다른 화면으로 이동했다가 돌아올 때 Observer가 OnGlobalLayoutListener를 다시 모니터링하기를 원합니다.

로그인 Fragment.kt

class LoginFragment : Fragment() {

    private var _binding: FragmentLoginBinding? = null
    private val binding get() = _binding!!
    private var waitTime = 0L
		// onDestroyView()에서 사용할 수 있도록 멤버변수로 선언
		private var listener: ViewTreeObserver.OnGlobalLayoutListener? = null

    @SuppressLint("ClickableViewAccessibility")
    override fun onCreateView(

        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        _binding = FragmentLoginBinding.inflate(inflater, container, false)

        initView()

				...

        return binding.root
}

override fun onResume() {
        super.onResume()

        initView()

    }

	...

    @SuppressLint("ClickableViewAccessibility")
    private fun initView() {

        val showDefaultHeight = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 150f,
					resources.displayMetrics
					).toInt()

        val hideDefaultHeight = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 150f,
					resources.displayMetrics
					).toInt()


				listener = ViewTreeObserver.OnGlobalLayoutListener {
            val viewHeight: Int = binding.loginLayout.height
            val rootHeight: Int = binding.loginLayout.rootView.height
            val diff = rootHeight - viewHeight

            Log.d("SH_TAG", "viewHeight = $viewHeight")
            Log.d("SH_TAG", "rootHeight = $rootHeight")
            Log.d("SH_TAG", "diff = $diff")
            Log.d("SH_TAG", "showDefaultHeight = $showDefaultHeight")
            Log.d("SH_TAG", "hideDefaultHeight = $hideDefaultHeight")


            if (diff > showDefaultHeight) {
                setSNSLoginUIVisibility(binding, View.GONE)
            } else if (diff < hideDefaultHeight) {
                Handler(Looper.getMainLooper()).postDelayed({
                    setSNSLoginUIVisibility(
                        binding,
                        View.VISIBLE
                    )
                }, 10)
            }
        }

        binding.loginLayout.viewTreeObserver.addOnGlobalLayoutListener(listener)

}

...

    override fun onStopView() {
		    super.onStop()
        binding.loginLayout.viewTreeObserver.removeOnGlobalLayoutListener(listener)

    }

}

내 기분

긴 내비게이터 애플리케이션 여정이 마침내 끝났습니다.

무언가를 배우는 데 정말 오랜 시간이 걸립니다.

그래서 남들보다 더 성실해야 한다.

또한 집중하는 데 문제가 있습니다.

아아, 생존은 쉽지 않다

그래도 마지막 순간에 나는 커피에서 힘과 집중력을 얻었습니다!

오늘도 수고했어요~~!