LoginFragment.kt에서 NullPointerException 발생
- 처음 이 에러를 맞닥드렸을 땐 제일 첫 번째 줄에 getBinding이 보여서, binding이 되지 않은건가? 하고 생각했었다.
- 그러나 알고보니 ViewTreeObserver와 OnGlobalLayout에 대하여 문제가 생겨서 발생한 에러였다
- 에러코드를 읽을 때 반드시 윗부분만 보지 않고 아래쪽도 좀 챙겨보는 습관을 길러보자!
LoginFragment.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의 height를 계산하여 soft keyboard위에 enter버튼을 적당한 위치에 생성하기 위해 만들어둔 것.
- ViewTreeObserver를 사용하는 사용법을 검색해보면 항상 따라오는 설명은 다음과 같다
- ViewTreeObserver를 사용하고 나서는 반드시 removeOnGlobalLayoutListener()를 구현해야 한다.
- 그렇지 많으면 Observer가 레이아웃에 대한 변경 감지를 무한대로 요청하기 때문에 memory leak이 발생한다.
- 단 한 번만 레이아웃에 대한 변경을 감지하고 싶다면 addOnGlobalLayoutListener안에 바로 removeOnGlobalLayoutListener를 구현해주면 된다.
- 그렇지 많으면 Observer가 레이아웃에 대한 변경 감지를 무한대로 요청하기 때문에 memory leak이 발생한다.
- ViewTreeObserver를 사용하고 나서는 반드시 removeOnGlobalLayoutListener()를 구현해야 한다.
- 하지만 나의 경우 지속적으로 변경을 감지하도록 놔두다가 다음 화면으로 넘어갔을 때 그만 감지하도록 만들고 싶었다.
- 더 나아가서 다시 원래 화면으로 되돌아왔을 때 다시 Observer가 작동했으면 좋겠다고 생각했다.
- 이런 경우엔 removeOnGlobalLayoutListener를 프래그먼트의 onStop() 생명주기에 구현해주면 된다.
- onStop()은 프래그먼트가 화면에서 사라졌을 때 호출되는 매서드
- 더 나아가서 다시 원래 화면으로 되돌아왔을 때 다시 Observer가 작동했으면 좋겠다고 생각했다.
내가 하고싶은 것?
나는 사용자가 ID창이나 PW창을 클릭해줄 때 마다 Observer를 통해 레이아웃의 크기를 감지하여 키보드 뒤 요소들을 모두 사라지게 하거나 다시 나타나게 하는 기능을 구현하고 싶다.
이 말인 즉슨, 계속해서 레이아웃에 대한 변경을 감지할 수 있게 만들고 싶다
더불어 다른 화면으로 이동했다가 다시 되돌아 왔을 땐 다시 OnGlobalLayoutListener에 대해 Observer가 또 다시 감시해주었으면 좋겠다.
LoginFragment.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)
}
}
느낀 점 🤔
Navigator를 적용하는 기나긴 여정이 드디어 끝났다.
정말 뭐 하나 배우는데에 오랜 시간이 걸리는 편..
그렇기에 다른 사람들보다 더 성실해야하지만
또 쉽게 집중하지 못하는 편..
아이고 살아남기 어렵다
그래도 막판에 커피에 힘을 얻어 집중 빡 완료!
오늘도 수고했습니다~~!
'📜 TIL' 카테고리의 다른 글
Fragment에서 뷰바인딩(ViewBinding)해주는 방법과 적용 (0) | 2023.03.12 |
---|---|
Soft Keyboard(virtual keyboard) 띄워졌을 때 키보드 바로 위에 버튼이 보여지도록 만들기 (0) | 2023.03.10 |
[Android] Navigation으로 Fragment간 이동을 편리하게 만들어주기 (0) | 2023.03.01 |
[Android] 서버 로그인 시 비밀번호 암호화 적용해주기 (0) | 2023.02.27 |
[Android] 안드로이드 개발자 면접질문 정리 part 1 (0) | 2023.02.22 |