美文网首页
【从BUG说起】Fragment viewbinding错误使用

【从BUG说起】Fragment viewbinding错误使用

作者: super可乐 | 来源:发表于2022-09-02 14:39 被阅读0次

写在前面:项目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.onDestroyViewonDestroy后(包含回调时)使用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.recyclerViewadapter肯定是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,即:

  1. Fragment.onDestroyView回调前,viewLifecycle.onDestroy已经回调,binding在viewLifecycle.onDestroy里被赋值为null
  2. 在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

相关文章

网友评论

      本文标题:【从BUG说起】Fragment viewbinding错误使用

      本文链接:https://www.haomeiwen.com/subject/aizenrtx.html