프래그먼트 내에서 ViewBinding 객체를 생성하고 메모리를 해제하는 것은 매우 중요하다. ViewBinding 객체는 프래그먼트 내에서 레이아웃에 정의된 모든 뷰들을 참조하며, 따라서 메모리를 제때 해제해주지 않으면 프래그먼트가 소멸될 때까지 계속 남아있기 때문에 메모리 릭이 발생한다. 직전 포스팅에서 프래그먼트 생명주기에 대해 설명했었는데, 프래그먼트 '자체'와 프래그먼트 '뷰'가 생성되고 소멸되는 시기는 각각 다르고, 따라서 onDestroyView()에서 프래그먼트 뷰가 소멸되어도 프래그먼트 자체는 여전히 메모리에 남아있기 때문에 뷰에 대한 모든 참조를 제거해야 한다고 했었다. 바로 이 때 ViewBinding 객체를 해제해줘야 한다.
안드로이드 공식 문서에선 ViewBinding 객체를 해제하는 방법으로 다음의 코드를 명시해놓았다. 매우 간단한 방법으로, onDestroyView() 내부에서 ViewBinding 객체를 null로 셋팅하는 것이다.
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
하지만 프래그먼트마다 ViewBinding 객체를 null로 만든다면 보일러 플레이트 코드가 많아질 수 있고, 메모리를 해제하지 못하는 실수를 범할 수도 있다.
구글에서는 위 방법 말고 ViewBinding 객체의 초기화를 위임하는 AutoClearedValue라는 방법도 제안했지만, 여러개의 방안들을 구글링을 통해 찾아보면서 나는 다음의 라이브러리를 사용하는 것이 ViewBinding 객체를 관리하기에 제일 편리하다고 생각했다. GitHub에 샘플 소스코드가 굉장히 잘 작성되어 있고, 코드 몇 줄만 바꿔주면 되기 때문에 더욱 추천하는 바이다.
ViewBindingPropertyDelegate를 사용하면 생성된 Binding 객체를 안전하게 관리할 수 있다. README에 명시되어 있는 ViewBindingPropertyDelegate의 장점들은 다음과 같다.
나는 Fragment와 DialogFragment에서 ViewBindingPropertyDelegate를 사용해 ViewBinding 객체를 생성해보았다. README에서 no reflection 방식을 사용하는 것이 성능에 더 도움이 된다고 해서 샘플 코드에서 no reflection 코드를 참고했다. 샘플 코드엔 프래그먼트 말고도 액티비티, RecyclerView 뷰홀더 클래스 내에서 ViewBindingPropertyDelegate을 사용하는 것에 대한 예제도 있으니 참고해서 필요한 부분에 사용하면 된다.
먼저 ViewBindingPropertyDelegate를 사용하기 위해선 build.gradle에 다음과 같은 레포지토리와 종속 항목을 추가해야 한다.
allprojects {
repositories {
mavenCentral()
}
}
dependencies {
// reflection-free flavor
implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'
}
class MemoFragment : Fragment(R.layout.fragment_memo) {
private val binding by viewBinding(FragmentMemoBinding::bind,
onViewDestroyed = {
// reset view
})
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
}
override fun onDestroyView() {
super.onDestroyView()
Log.e("MemoFragment", "onDestroyView()")
}
}
onViewDestroyed는 Binding 객체가 소멸될 때 호출된다. 따라서 { } 내부에는 Binding 객체가 소멸될 때 해제해주어야 하는 작업을 코드로 작성해주면 된다.
onViewDestroyed()와 onDestroyView()가 호출되는 순서가 궁금해서 로그로 찍어보니 다음과 같았다. 뷰가 소멸된 이후에 Binding 객체가 해제되는 듯 하다.
class MainDialog() : DialogFragment() {
private val binding by viewBinding(MainDialogBinding::bind)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.main_dialog, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
...
}
}
https://cliearl.github.io/posts/android/prevent-fragment-memory-leak-during-viewbinding/
https://yoon-dailylife.tistory.com/57
https://stackoverflow.com/questions/66119231/is-it-necessary-to-set-viewbinding-to-null-in-fragments-ondestroy/66119393#66119393
https://stackoverflow.com/questions/35520946/leak-canary-recyclerview-leaking-madapter