如何优雅的在Fragment中使用ViewBinding
前言
在Fragment中控制View十分简单,只需要声明+findViewById
即可:
class FragmentA : Fragment() {
private lateinit var imageView: ImageView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imageView = view.findViewById(R.id.imageView)
}
}
但这样同时也遇到了一个问题:在使用Navigation或者使用replace
并addToBackStack
进行FragmentA切换到FragmentB时,FragmentA会走到onDestroyView
,但不会destory
。FragmentA走到onDestroyView
时,Fragment会对根View的引用置空,由于imageView
被Fragment持有,所以此时imageView
并未被释放,从而导致了内存泄漏。
View Leak 1 | View Leak 2 |
---|
当页面变的复杂时,变量的声明以及赋值也会变成一个重复的工作。比较成熟的框架如Butter Knife通过@BindView
注解生成代码,以避免手工编写findViewById
代码,同时也提供了Unbinder
用以在onDestoryView
中进行解绑以防止内存泄漏。不过在Butter Knife的官方文档中提到目前Butter Knife已不再维护,推荐使用ViewBinding
作为视图绑定工具:
Attention: This tool is now deprecated. Please switch to view binding. Existing versions will continue to work, obviously, but only critical bug fixes for integration with AGP will be considered. Feature development and general bug fixes have stopped.
在ViewBinding的官方文档中,推荐的写法如下:
class TestFragment : Fragment() {
private var _binding: FragmentTestBinding? = null
// 只能在onCreateView与onDestoryView之间的生命周期里使用
private val binding: FragmentTestBinding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTestBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
这种方式虽然防止了内存泄漏,但仍然需要手工编写一些重复代码,大部分人甚至可能直接声明lateinit var binding
,从而导致更严重的内存泄漏问题。下面我们将介绍两种解放方案:
Fragment基类
如果项目中存在一个BaseFragment
的话,我们完全可以将上面的逻辑放在BaseFragment
中:
open class BaseFragment<T : ViewBinding> : Fragment() {
protected var _binding: T? = null
protected val binding: T get() = _binding!!
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
或者更进一步,将onCreateView
的逻辑也放在父类中:
abstract class BaseFragment<T : ViewBinding> : Fragment() {
private var _binding: T? = null
protected val binding: T get() = _binding!!
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Bundle?) -> T
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = bindingInflater.invoke(inflater, container, savedInstanceState)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
子类使用时:
class TestFragment : BaseFragment<FragmentTestBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Bundle?) -> FragmentTestBinding
get() = { layoutInflater, viewGroup, _ ->
FragmentTestBinding.inflate(layoutInflater, viewGroup, false)
}
}
不过这种方式由于给基类增加了泛型,所以对于已有项目的侵入性比较高。
生命周期委派
借助Kotlin的by
关键字,我们可以将binding
置空的任务交给Frament生命周期进行处理,比较简单的版本如下:
class LifecycleAwareViewBinding<F : Fragment, V : ViewBinding> : ReadWriteProperty<F, V>, LifecycleEventObserver {
private var binding: V? = null
override fun getValue(thisRef: F, property: KProperty<*>): V {
binding?.let {
return it
}
throw IllegalStateException("Can't access ViewBinding before onCreateView and after onDestroyView!")
}
override fun setValue(thisRef: F, property: KProperty<*>, value: V) {
if (thisRef.viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.DESTROYED) {
throw IllegalStateException("Can't set ViewBinding after onDestroyView!")
}
thisRef.viewLifecycleOwner.lifecycle.addObserver(this)
binding = value
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
binding = null
source.lifecycle.removeObserver(this)
}
}
}
在使用时可以直接通过by
关键字,但仍需在onCreateView
中进行赋值:
class TestFragment : Fragment() {
private var binding: FragmentTestBinding by LifecycleAwareViewBinding()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentTestBinding.inflate(inflater, container, false)
return binding.root
}
}
如果想省略onCreateView
中的创建ViewBinding
的重复逻辑,有两种思路,一个是Fragment构造时传入布局Id,通过viewBinding生成的bind
函数创建ViewBinding
;另外一种思路则是通过反射调用ViewBinding
的inflate
方法。两种思路的主要不同就是创建ViewBinding
的方式不一样,而核心代码一样,实现如下:
class LifecycleAwareViewBinding<F : Fragment, V : ViewBinding>(
private val bindingCreator: (F) -> V
) : ReadOnlyProperty<F, V>, LifecycleEventObserver {
private var binding: V? = null
override fun getValue(thisRef: F, property: KProperty<*>): V {
binding?.let {
return it
}
val lifecycle = thisRef.viewLifecycleOwner.lifecycle
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
this.binding = null
throw IllegalStateException("Can't access ViewBinding after onDestroyView")
} else {
lifecycle.addObserver(this)
val viewBinding = bindingCreator.invoke(thisRef)
this.binding = viewBinding
return viewBinding
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
binding = null
source.lifecycle.removeObserver(this)
}
}
}
然后创建函数返回LifecycleAwareViewBinding
即可:
// 1. 通过bind函数
fun <V : ViewBinding> Fragment.viewBinding(binder: (View) -> V): LifecycleAwareViewBinding<Fragment, V> {
return LifecycleAwareViewBinding { binder.invoke(it.requireView()) }
}
// 使用
class TestFragment : Fragment(R.layout.fragment_test) {
private val binding: FragmentTestBinding by viewBinding(FragmentTestBinding::bind)
}
// 2. 通过反射的方式
inline fun <reified V : ViewBinding> Fragment.viewBinding(): LifecycleAwareViewBinding<Fragment, V> {
val method = V::class.java.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
return LifecycleAwareViewBinding { method.invoke(null, layoutInflater, null, false) as V }
}
// 使用
class TestFragment : Fragment() {
private val binding: FragmentTestBinding by viewBinding()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return binding.root
}
}
需要注意的是第一种方式使用了Fragment#requireView
方法,所以需要将布局id传给Fragment
的构造方法(将布局id传给Fragment实际上是借助了Fragment
默认的onCreateView
实现,虽然不传布局Id、手动实现也可以,但这样实际上和最上面提到的方法差不多了)。
上面的两种思路GitHub中已经有作者实现了,并且考虑了一些边界case以及优化,感兴趣的可以去看看:ViewBindingPropertyDelegate
总结
对于ViewBinding
为了防止内存泄漏而出现的模板代码,可以将模板代码提取至基类Fragment中或者借助Fragment的viewLifecycleOwner
的生命周期进行自动清理;对于onCreateView
中为了创建ViewBinding
而出现的模板代码,可以借助Fragment#onCreateView
的默认实现以及ViewBinding
生成的bind
函数进行创建,或者通过反射调用ViewBinding
生成的inflate
方法创建ViewBinding
。