如何优雅的在Fragment中使用ViewBinding

Aengus Sun 212 2022-01-25

前言

在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或者使用replaceaddToBackStack进行FragmentA切换到FragmentB时,FragmentA会走到onDestroyView,但不会destory。FragmentA走到onDestroyView时,Fragment对根View的引用会置空,由于imageView被Fragment持有,所以此时imageView并未被释放,从而导致了内存泄漏。

View Leak 1View 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;另外一种思路则是通过反射调用ViewBindinginflate方法。两种思路的主要不同就是创建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和优化,以及增加了对DialogFragment的支持,感兴趣的可以去看看:ViewBindingPropertyDelegate

总结

对于ViewBinding为了防止内存泄漏而出现的模板代码,可以将模板代码提取至基类Fragment中或者借助Fragment的viewLifecycleOwner的生命周期进行自动清理;对于onCreateView中为了创建ViewBinding而出现的模板代码,可以借助Fragment#onCreateView的默认实现以及ViewBinding生成的bind函数进行创建,或者通过反射调用ViewBinding生成的inflate方法创建ViewBinding


# Java # 安卓 # Jetpack