View Binding and Fragments

A fragment may outlive its views. It is possible for a fragment to:

In between the first onDestroyView() and the second onCreateView(), ideally our fragment has no references to the views that were created and destroyed from the first onCreateView()/onDestroyView() cycle. In the specific case of view binding, this means that we do not want to hold onto our binding object after onDestroyView() gets called.

The recommended pattern is to set the Java field or Kotlin property that holds the binding object to null in onDestroyView(). The mechanics of this will vary a bit between Java and Kotlin, owing to Kotlin caring deeply about null.

Java

The Java edition of ListFragment has a binding field:

  private TodoRosterBinding binding;

We then set a value to it in onCreateView(), using TodoRosterBinding.inflate():

  @Nullable
  @Override
  public View onCreateView(@NonNull LayoutInflater inflater,
                           @Nullable ViewGroup container,
                           @Nullable Bundle savedInstanceState) {
    binding = TodoRosterBinding.inflate(inflater, container, false);

    return binding.getRoot();
  }

We can then use it in places like onViewCreated():

  @Override
  public void onViewCreated(@NonNull View view,
                            @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    ListViewModel vm = new ViewModelProvider(this).get(ListViewModel.class);

    binding.items.setLayoutManager(new LinearLayoutManager(getContext()));

    ToDoListAdapter adapter = new ToDoListAdapter(getLayoutInflater(),
      this::navTo);

    adapter.submitList(vm.items);
    binding.items.setAdapter(adapter);
  }

Finally, we can set the field back to null in onDestroyView():

  @Override
  public void onDestroyView() {
    super.onDestroyView();

    binding = null;
  }

As a result, after onDestroyView(), the widgets created in onCreateView() can be garbage-collected.

Kotlin

The Java approach results in a fair bit of boilerplate. The Kotlin example isolates a lot of that in a common ViewBindingFragment class.

ViewBindingFragment

ViewBindingFragment resides in the Utilities module that we examined in a previous chapter. It handles inflating a layout using a view binding for you, as well as setting that view binding’s property to null in onDestroyView():

package com.commonsware.jetpack.sampler.util

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding

abstract class ViewBindingFragment<Binding : ViewBinding>(
  private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> Binding
) : Fragment() {
  private var binding: Binding? = null

  override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    return bindingInflater(inflater, container, false)
      .apply { binding = this }
      .root
  }

  override fun onDestroyView() {
    binding = null

    super.onDestroyView()
  }

  protected fun requireBinding(): Binding = binding
    ?: throw IllegalStateException("You used the binding before onCreateView() or after onDestroyView()")

  protected fun useBinding(bindingUse: (Binding) -> Unit) {
    bindingUse(requireBinding())
  }
}

ViewBindingFragment uses generics. The Binding generic type is the generated view binding class associated with the layout for this fragment (e.g., TodoRosterBinding). Those classes all extend from a Jetpack-supplied ViewBinding base class, so Binding is declared as being a sub-type of ViewBinding.

You can learn more about Kotlin generics in the "Generics" chapter of Elements of Kotlin!

The constructor takes a function type as a parameter. The odd-looking function signature is the one used by the inflate() functions on the generated ViewBinding classes. So, subclasses need to pass a function reference to that inflate() function:

class ListFragment :
  ViewBindingFragment<TodoRosterBinding>(TodoRosterBinding::inflate) {

ViewBindingFragment than has its own onCreateView() and onDestroyView() functions. onCreateView() calls that bindingInflater function type to inflate the view binding, holds the value in the binding property, and returns the root View to satisfy the onCreateView() contract. onDestroyView() simply sets binding to be null.

However, binding is private. Subclases can use it in one of two ways:

ListFragment

As a result, subclasses of ViewBindingFragment can focus on their business logic and let ViewBindingFragment take care of inflating and clearing the view binding object.

ListFragment, for example, has ViewBindingFragment manage the TodoRosterBinding and just works with its widgets:

package com.commonsware.jetpack.sampler.fragments

import android.os.Bundle
import android.view.View
import androidx.fragment.app.commit
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.commonsware.jetpack.sampler.fragments.databinding.TodoRosterBinding
import com.commonsware.jetpack.sampler.util.ViewBindingFragment

class ListFragment :
  ViewBindingFragment<TodoRosterBinding>(TodoRosterBinding::inflate) {
  private val vm: ListViewModel by viewModels()

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    useBinding { binding ->
      binding.items.layoutManager = LinearLayoutManager(context)

      val adapter = ToDoListAdapter(layoutInflater) {
        navTo(it)
      }

      binding.items.adapter = adapter.apply { submitList(vm.items) }
    }
  }

  private fun navTo(model: ToDoModel) {
    parentFragmentManager.commit {
      replace(android.R.id.content, DisplayFragment.newInstance(model.id))
      addToBackStack(null)
    }
  }
}

DisplayFragment

Similarly, DisplayFragment lets ViewBindingFragment manage the TodoDisplayBinding object:

package com.commonsware.jetpack.sampler.fragments

import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import com.commonsware.jetpack.sampler.fragments.databinding.TodoDisplayBinding
import com.commonsware.jetpack.sampler.util.ViewBindingFragment

private const val ARG_MODEL_ID = "modelId"

class DisplayFragment :
  ViewBindingFragment<TodoDisplayBinding>(TodoDisplayBinding::inflate) {
  companion object {
    fun newInstance(modelId: String) = DisplayFragment().apply {
      arguments = bundleOf(ARG_MODEL_ID to modelId)
    }
  }

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val vm: DisplayViewModel by viewModels()
    val model = vm.getModel(
      arguments?.getString(ARG_MODEL_ID)
        ?: throw IllegalStateException("no modelId provided!")
    )

    model?.let {
      useBinding { binding ->
        binding.model = model

        binding.createdOnFormatted = DateUtils.getRelativeDateTimeString(
          activity,
          model.createdOn.toEpochMilli(), DateUtils.MINUTE_IN_MILLIS,
          DateUtils.WEEK_IN_MILLIS, 0
        )
      }
    }
  }
}

Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.