preface

Controlling a View in a Fragment is as simple as declaring +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)
    }
}
Copy the code

There is a problem with this: When you switch FragmentA to FragmentB using Navigation or replace and addToBackStack, the FragmentA will go onDestroyView, but not deStory. When FragmentA goes onDestroyView, the Fragment’s reference to the root View will be empty. Since the imageView is held by the Fragment, the imageView will not be freed, causing a memory leak.

As the page gets more complex, variable declaration and assignment can become a repetitive task. Mature frameworks such as Butter Knife generate code with @BindView annotations to avoid writing findViewById code by hand, and provide unbinders to unbind onDestoryView to prevent memory leaks. However, as mentioned in the official Butter Knife documentation, Butter Knife is no longer maintained and ViewBinding is recommended as a ViewBinding tool:

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.

In the official ViewBinding documentation, the recommended way to write this is:

class TestFragment : Fragment() {
    private var _binding: FragmentTestBinding? = null
    // Can only be used in the life cycle between onCreateView and 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(a) {
        super.onDestroyView()
        _binding = null}}Copy the code

This prevents memory leaks, but still requires some repetitive code to be written by hand, and most people might even declare the LateInit var binding directly, leading to more serious memory leaks. Here are two options for liberation:

Fragments base class

If there is a BaseFragment in the project, we can put the above logic in the BaseFragment:

open class BaseFragment<T : ViewBinding> : Fragment() {

    protected var _binding: T? = null
    
    protected val binding: T get() = _binding!!

    override fun onDestroyView(a) {
        super.onDestroyView()
        _binding = null}}Copy the code

Or take it a step further and put the logic of onCreateView in the parent class as well:

abstract class BaseFragment<T : ViewBinding> : Fragment() {

    private var _binding: T? = null
    protected val binding: T get() = _binding!!

    abstract valbindingInflater: (LayoutInflater, ViewGroup? , Bundle?) -> Toverride fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View {
        _binding = bindingInflater.invoke(inflater, container, savedInstanceState)
        return binding.root
    }

    override fun onDestroyView(a) {
        super.onDestroyView()
        _binding = null}}Copy the code

When subclasses are used:

class TestFragment : BaseFragment<FragmentTestBinding>() {
    
    override valbindingInflater: (LayoutInflater, ViewGroup? , Bundle?) -> FragmentTestBindingget() = { layoutInflater, viewGroup, _ ->
            FragmentTestBinding.inflate(layoutInflater, viewGroup, false)}}Copy the code

However, this approach is more intrusive for existing projects because it adds generics to the base class.

Lifecycle delegation

Using Kotlin’s by keyword, we can assign the binding null task to the Frament lifecycle. The simpler version is as follows:

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)}}}Copy the code

You can use the by keyword directly, but you still need to assign it in 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
    }
}
Copy the code

If you want to avoid repeating the logic in onCreateView for creating a ViewBinding, there are two ways to create a ViewBinding. One is to pass the layout Id to the Fragment and create a ViewBinding using the bind function generated by the ViewBinding. Another idea is to call the inflate method of a ViewBinding through reflection. The main difference between the two approaches is the way to create a ViewBinding, and the core code is the same, which is implemented as follows:

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)}}}Copy the code

Then create a function returns LifecycleAwareViewBinding can:

// 1. Bind
fun <V : ViewBinding> Fragment.viewBinding(binder: (View) - >V): LifecycleAwareViewBinding<Fragment, V> {
    return LifecycleAwareViewBinding { binder.invoke(it.requireView()) }
}
/ / use
class TestFragment : Fragment(R.layout.fragment_test) {
    private val binding: FragmentTestBinding by viewBinding(FragmentTestBinding::bind)
}

// 2. By reflection
inline fun <reified V : ViewBinding> Fragment.viewBinding(a): 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 }
}
/ / use
class TestFragment : Fragment() {
    private val binding: FragmentTestBinding by viewBinding()
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View {
        return binding.root
    }
}
Copy the code

Note that the first method uses the Fragment#requireView method, so you need to pass the layout ID to the Fragment constructor. (Passing the layout ID to the Fragment is actually implemented using the Fragment’s default onCreateView implementation. You can do it manually without passing the layout Id, but this is actually pretty much the same as the above method).

The above two kinds of ideas in making already has the author realize, and consider some boundary case and optimization, interested can look at the: ViewBindingPropertyDelegate

conclusion

For template code generated by ViewBinding to prevent memory leaks, the template code can be extracted into the Fragment base class or cleaned up automatically by the viewLifecycleOwner of the Fragment. Template code that appears in onCreateView to create a ViewBinding can be created using the default implementation of Fragment#onCreateView and the bind function generated by ViewBinding. Or create a ViewBinding by calling the inflate method generated by the ViewBinding through reflection.