(안드로이드) 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) } }

내 기분

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

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

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

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

아아, 생존은 쉽지 않다

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

오늘도 수고했어요~~!