写在前面:项目
ViewBinding
是基于Kotlin delegate的实现
问题:为什么onDestroyView里的binding.recyclerView里面的adapter是空的?
前些天,同事求助:为什么onDestroyView里的binding.recyclerView里面的adapter是空的?我明明已经设置了啊?
//xxxFragment.kt
private val binding by viewBindings(XXXBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
……
binding.recyclerView.run {
……
//set adapter
adapter = XXXAdapter()
……
}
……
}
override fun onDestroyView() {
super.onDestroyView()
……
//np exception!! adapter is null
binding.recyclerView.adapter.notifyDataChanged()
}
为什么recyclerView的adapter已经设置了,为啥在onDestroyView时,adapter是null呢?
要知道原因,我们得从项目实现代码来分析。
1、Fragment引入by
(delegate)实现的原因
对于Fragment
生命周期,大家都知道熟悉了解,这里简单说下:
- Fragment以
replace
方式添加时,View的生命周期与Fragment的生命周期不一致 - Fragment没有
onDestroy
,而是走onDestroyView
,即View被dettached了 - 如果
ViewBinding
实例作为Fragment的成员变量,那么ViewBinding实例并没有在onDestroyView时相应的被释放,ViewBinding引用的资源也没有相应释放
Kotlin的delegate
方式结合Fragment.viewLifecycle
就很好解决此问题:
- binding的通过by,委派类来实例化binding
- viewLifecycler.onDestroy时,释放委派类的binding实例
2、项目原viewBinding的实现
//FragmentExtension.kt
//简化版,后面会给出全部实现
inline fun <VB : ViewBinding> Fragment.viewBinding(
crossinline viewBinder: (View) -> VB
) = ViewBindingDelegate{
viewBinder(it.requireView())
}
class ViewBindingDelegate<VB : ViewBinding>(
private val viewBinder: (Fragment) -> VB
) : ReadOnlyProperty<Fragment, VB> {
//binding will auto clear after onDestroyView
private var binding: VB? = null
……
private fun setLifecycleObserver(fragment: Fragment) {
……
viewLifecycleOwner = fragment.viewLifecycleOwnerLiveData.value
if (viewLifecycleOwner != null) {
//添加viewLifecycle的Observer
viewLifecycleOwner.lifecycle.addObserver(getViewLifecycleObserver())
} else {
//省略,后面附全实现代码
}
……
}
private fun getViewLifecycleObserver(): DefaultLifecycleObserver {
viewLifecycleObserver?.let {
return it
}
return object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
//在Fragment.onDestroyView时,释放binding实例
binding = null
}
}.also { viewLifecycleObserver = it }
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): VB {
binding?.let {
return it
}
……
setLifecycleObserver(thisRef)
return viewBinder(thisRef).also { this.binding = it }
}
}
2.1、项目原viewBinding实现存在的Bug
从代码看,好像没问题:binding
实例确实在viewLifecycle.onDestroy
时释放了。
但实质却存在着巨大的bug:
- 当在
Fragment.onDestroyView
或onDestroy
后(包含回调时)使用ViewBinding
,binding实例在被viewLifecycle
释放后,又重新实例化新的binding对象!
比如文章开头同事求助的代码:
//XXXFragment.kt
override fun onDestroyView() {
super.onDestroyView()
……
//np exception!! adapter is null
binding.recyclerView.adapter.notifyDataChanged()
}
在onDestroyView里使用binding,这时的binding是新创建出来的,并非onViewCreated
是创建的。因为旧binding实例已经在viewLifecycle.onDestroy
已经被释放了:
private fun getViewLifecycleObserver(): DefaultLifecycleObserver {
viewLifecycleObserver?.let {
return it
}
return object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
//在Fragment.onDestroyView时,释放binding实例
binding = null
}
}.also { viewLifecycleObserver = it }
}
所以binding.recyclerView
的adapter
肯定是null的!
2.2 为什么会是新的binding实例?
要回答这个问题,得先从Fragment的调用栈来看:
//Fragment.java
void performDestroyView() {
mChildFragmentManager.dispatchDestroyView();
if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()
.isAtLeast(Lifecycle.State.CREATED)) {
mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
……
onDestroyView();
……
}
可见调用栈:Fragment.performDestroyView->viewLifecycle.onDestroy->Fragment.onDestroyView
,即:
- 在
Fragment.onDestroyView
回调前,viewLifecycle.onDestroy
已经回调,binding在viewLifecycle.onDestroy里被赋值为null
- 在Fragment.onDestoryView里再使用
binding.recyclerView
,实质是调用delegate的getValue方法,由于viewLifecycler.onDestroy应将binding = null
,在getValue时,发现binding = null,重新创建binding
实例
到这里,我们知道为什么会创建新的binding实例。同时由于这种情况的使用,导致失去引入viewBindings的delegate
意义: onDestroyView时,使用并viewBinding会导致创建新的binding实例,导致没有释放binding实例
3、viewBinding实现的优化
很多时候,我们需要在onDestroyView时,对view做些clear操作,这时候就得使用的binding对象,例如同事的代码:
//XXXFragment.kt
override fun onDestroyView() {
super.onDestroyView()
……
binding.recyclerView.adapter.notifyDataChanged()
}
经过上面分析,我们知道在onDestroyView
使用binding,会得不到想要的效果:
创建新binding实例,adapter = null
但我们确实需要在onDestroyView时使用binding.recyclerView
!那怎么办呢?
3.1 优化:允许在Fragment.onDestroyView使用binding对象
为了允许在onDestroyView时使用binding对象,我们对delegate的实现稍微修改下即可满足:
//FragmentExtension.kt
private fun getViewLifecycleObserver(): DefaultLifecycleObserver {
viewLifecycleObserver?.let {
return it
}
return object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
//post runnable, 让binding在下帧刷新时再释放
Handler(Looper.getMainLooper()).post {
binding = null
}
}
}.also { viewLifecycleObserver = it }
}
由于binding = null被Handler post runnable
,即在下一帧刷新是执行,所以在Fragment.onDestroyView
时,binding还是没有被设置为null,那么在Fragment.onDestroyView回调时使用binding就没有问题了。
3.2 在Fragment.onDestroyView后使用呢?
从合理的角度出发,在Fragment.onDestroyView后,我们时不应该再使用binding(限delegate方式),因为View已经destroyed(dettached)了,在onCreateView时,又重新创建新的View了。
那么就必须对这种错误的使用binding时机,进行限制:
//FragmentExtension.kt
override fun getValue(thisRef: Fragment, property: KProperty<*>): VB {
binding?.let {
return it
}
if (BuildConfig.DEBUG) {
//throw exception for incorrect use of ViewBinding in develop mode
//throw exception : use binding after onDestroyView(in onDestroyView is OK)
if (thisRef.lifecycle.currentState >= Lifecycle.State.CREATED &&
thisRef.viewLifecycleOwnerLiveData.value == null
) {
throw RuntimeException("can not use viewBinding after onDestroyView(in onDestroyView is OK)")
}
//throw exception : use binding after when fragment destroyed
if (thisRef.lifecycle.currentState == Lifecycle.State.DESTROYED) {
throw RuntimeException("can not use viewBinding after fragment destroyed(in onDestroy is not allow)")
}
setLifecycleObserver(thisRef)
}
return viewBinder(thisRef).also { this.binding = it }
}
错误时机使用,强制抛出异常!
4、总结
在kotlin代码里,viewbinding
多数情况下是使用by(delegate)
的方式实现的,但对新人来说,很容易错误的使用binding导致问题,同时也一头雾水的问:为什么?
本文确实针对项目中遇到的真实问题,进行了代码优化,希望对大家有帮助
5、附录:viewBinding的实现
//FragmentExtension.kt
/***
* Do not use ViewBinding before [Fragment.onAttach] or after [Fragment.onDestroyView]
* Using in [Fragment.onAttach] or [Fragment.onDestroyView] is OK.
* if you want use binding anywhere when created, use [Fragment.viewBindings(false,viewBinder)]
* usage : val binding by viewBindig(false,XXXBinding::inflate)
*/
@JvmName("fragmentViewBindingInflate")
@Suppress("unused")
inline fun <reified VB : ViewBinding> Fragment.viewBinding(crossinline viewBinder: (LayoutInflater) -> VB) =
viewBinding(true, viewBinder)
/**
* if autoClear == false, you can use the binding anywhere when it created.
* But must notice that : the binding will maintains cross the whole life of the fragment
*
* if autoClear == true, Do not use ViewBinding before [Fragment.onAttach] or after [Fragment.onDestroyView]
* Using in [Fragment.onAttach] or [Fragment.onDestroyView] is OK.
* usage : val binding by viewBindig(false,XXXBinding::inflate)
*
* @param autoClear auto clear the binding instance if true
* @param viewBinder the view binding binder method
*/
@JvmName("fragmentViewBindingInflate")
@Suppress("unused")
inline fun <reified VB : ViewBinding> Fragment.viewBinding(
autoClear: Boolean = true,
crossinline viewBinder: (LayoutInflater) -> VB
) = ViewBindingDelegate(autoClear) {
//will throw exception using binding before Fragment.onAttach or after Fragment.onDetach
viewBinder(it.layoutInflater)
}
/***
* Do not use ViewBinding before [Fragment.onViewCreated] or after [Fragment.onDestroyView]
* but using in [Fragment.onViewCreated] or [Fragment.onDestroyView] is OK
* If you want use binding anywhere when created, use [Fragment.viewBindings(false,viewBinder)]
*
* Usage : FragmentXXX : [Fragment(R.layout.layout_xml)] (or you manual inflate view in onCreateView)
* Then you can use viewBinding in [Fragment.onViewCreated] or after this state:
* val binding by viewBindig(XXXBinding::bind)
*/
@JvmName("fragmentViewBindingBind")
@Suppress("unused")
inline fun <reified VB : ViewBinding> Fragment.viewBinding(crossinline viewBinder: (View) -> VB) =
viewBinding(true, viewBinder)
/**
* Do not use ViewBinding before [Fragment.onViewCreated]
* if autoClear == false, you can use the binding anywhere when it created.
* But must notice that : the binding will maintains cross the whole life of the fragment
*
* if autoClear == true, you can not use ViewBinding after [Fragment.onDestroyView],
* using in [Fragment.onDestroyView]is OK
*
* Usage : FragmentXXX : [Fragment(R.layout.layout_xml)] (or you manual inflate view in onCreateView)
* Then you can use viewBinding in onViewCreated or after this state:
* val binding by viewBindig(false,XXXBinding::bind)
*
* @param autoClear auto clear the binding instance if true
* @param viewBinder the view binding binder method
*/
@JvmName("fragmentViewBindingBind")
@Suppress("unused")
inline fun <reified VB : ViewBinding> Fragment.viewBinding(
autoClear: Boolean = true,
crossinline viewBinder: (View) -> VB
) = ViewBindingDelegate(autoClear) {
//will throw exception using binding before Fragment.onViewCreated
viewBinder(it.requireView())
}
class ViewBindingDelegate<VB : ViewBinding>(
private val autoClear: Boolean = true,
private val viewBinder: (Fragment) -> VB
) : ReadOnlyProperty<Fragment, VB> {
//binding will auto clear after onDestroyView
private var binding: VB? = null
private var isFragmentLifecycleObserverAdded: Boolean = false
//create viewLifecycleObserver when need
private var viewLifecycleObserver: DefaultLifecycleObserver? = null
private fun setLifecycleObserver(fragment: Fragment) {
var viewLifecycleOwner: LifecycleOwner? = null
try {
//if viewLifecycleOwner is null, it will throw exception, so just catch it and use live data observer instead
//normally it will not null,because it create in Fragment.performCreate and before onViewCreate
//so it works well in mostly conditions
viewLifecycleOwner = fragment.viewLifecycleOwner
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
Log.w("FragmentBinding", "call view binding before onViewCreate or onDestroyView??")
}
}
viewLifecycleOwner = viewLifecycleOwner ?: fragment.viewLifecycleOwnerLiveData.value
if (viewLifecycleOwner != null) {
viewLifecycleOwner.lifecycle.addObserver(getViewLifecycleObserver())
} else {
if (isFragmentLifecycleObserverAdded) {
return
}
isFragmentLifecycleObserverAdded = true
//normally viewLifecycleOwner will not null.
//viewLifecycleOwner is null, it means call view binding before onViewCreate
//so we add viewLifecycleOwnerLiveData observer
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(owner) {
it?.lifecycle?.addObserver(getViewLifecycleObserver())
}
}
})
}
}
private fun getViewLifecycleObserver(): DefaultLifecycleObserver {
viewLifecycleObserver?.let {
return it
}
return object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
//post clear binding, make sure it clear after fragment onDestroyView
Handler(Looper.getMainLooper()).post {
binding = null
}
}
}.also { viewLifecycleObserver = it }
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): VB {
binding?.let {
return it
}
if (autoClear) {
if (BuildConfig.DEBUG) {
//throw exception for incorrect use of ViewBinding in develop mode
//throw exception : use binding after onDestroyView(in onDestroyView is OK)
if (thisRef.lifecycle.currentState >= Lifecycle.State.CREATED &&
thisRef.viewLifecycleOwnerLiveData.value == null
) {
throw RuntimeException("can not use viewBinding after onDestroyView(in onDestroyView is OK)")
}
//throw exception : use binding after when fragment destroyed
if (thisRef.lifecycle.currentState == Lifecycle.State.DESTROYED) {
throw RuntimeException("can not use viewBinding after fragment destroyed(in onDestroy is not allow)")
}
}
setLifecycleObserver(thisRef)
}
return viewBinder(thisRef).also { this.binding = it }
}
}
作者:harlin
链接:https://juejin.cn/post/7137713268851736606
网友评论