이전 글에서 View Binding이 무엇인지와 Activity, Fragment에서의 사용법을 알아봤습니다.
하지만, Fragment에서 View Binding을 사용시 발생하는 이슈가 있어 해당 이슈를 알아보는 글을 포스팅 하려고 합니다.
View Binding in Fragment
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
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
}
안드로이드 공식 문서에서 Fragment에서 View Binding의 사용법을 위와 같은 샘플 코드로 제시하고 있습니다.
위처럼 코드를 작성하는 이유는 Fragment에서 View Binding을 사용할 경우 Fragment는 View보다 오래 지속되어,
Fragment의 Lifecycle로 인해 메모리 누수가 발생할 수 있기 때문입니다.
예를들어 Fragment에서 Navigation component 또는 BackStack or detach를 사용하는 경우, onDestroyView() 이후에 Fragment view는 종료되지만, Fragment는 여전히 살아 있습니다. 즉 메모리 누수가 발생하게 됩니다.
▶ 그래서 반드시 binding 변수를 onDetsroyView() 이후에 null로 만들어 줘야합니다.
Fragments outlives their views?
구글은 Fragment의 재사용을 위해 View들을 메모리에 보관하도록 Fragment의 동작을 변경하였습니다.
그래서 onDestroy() - onDestroyView()가 호출되고 나서도 View Binding에 대한 참조를 가비지 컬렉터에서 가비지 컬렉션을 명확하게 해 줄 필요가 있습니다.
우리는 이러한 코드들을 Fragment의 특성 때문에 매번 추가해주는 것은 원치 않을 것입니다.
여기에는 몇 가지 해결책들이 존재합니다.
private lateinit var textView: TextView
fun onViewCreated (view: View, savedInstanceState: Bundle) {
val binding = ResultProfileBinding.bind(view)
textView = binding.textView
}
▶ 이 방식은 Type Safe하지 않고, 성능도 손해를 보는 부분이 있습니다. 하지만 View Binding의 이슈에는 자유로울 수 있습니다.
Class ProfileFragment : Fragment(R.layout.profile_layout) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = ProfileLayoutBinding.bind(view)
binding.textName.text = "ch8n"
}
}
▶ 위의 코드처럼 레이아웃이 이미 확장되어 있다면 inflate() 대신에 View Binding의 정적bind() 메소드를 호출하면 됩니다.
bind()
bind() 메소드의 내부는 위와 같습니다.
해당 binding의 생성된 View 필드를 초기화하기 위해 전달한 View에서 내부적으로 findViewById를 사용합니다.
※ 주의할 점은 bind() 함수를 여러 번 호출하지 않아야 합니다.
→ bind() 함수가 호출될 때마다 모든 binding field를 다시 초기화하는 findViewById가 Trigger 될 수 있습니다.
class AutoClearedValue<T : Any>(val fragment: Fragment) : ReadWriteProperty<Fragment, T> {
private var _value: T? = null
init {
fragment.lifecycle.addObserver(object: DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner?.lifecycle?.addObserver(object: DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
_value = null
}
})
}
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return _value ?: throw IllegalStateException(
"should never call auto-cleared-value get when it might not be available"
)
}
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
_value = value
}
}
/**
* Creates an [AutoClearedValue] associated with this fragment.
*/
fun <T : Any> Fragment.autoCleared() = AutoClearedValue<T>(this)
▶ 위의 코드는 구글의 아키텍처 샘플 코드인 AutoCleardValue입니다.
해당 코드를 사용하기 위해선 build.gradle(app)에 dependencies를 추가해줘야 합니다.
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
Usage
▶ AutoCleardValue의 사용법은 다음과 같습니다.
// View binding
private var binding by autoCleared<FragmentUserProfileBinding>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentUserProfileBinding.inflate(inflater, container, false)
return binding.root
}
// Using with binding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.txtMain.text = "Hello World!"
}
하지만, 위와 같은 코드는 작성하기 쉽지 않습니다.
또한 Fragment의 코드 몇 줄을 줄이기 위해 delegate patern을 적용한 N 줄을 추가하는 것이 큰 이점이 있는 것인가에 대한 의견들도 있습니다.
그래서 몇몇 개발자들은 View Binding을 편하게 사용하기 위해 본인의 library를 만들어 코드에 적용하고 있는 모습들도 확인할 수 있습니다.
▶ BaseActivity, BaseFragment 클래스를 만들어 반복되는 코드들을 줄여줄 수 있습니다.
Simple one-liner ViewBinding in Fragments and Activities with Kotlin