View Binding and Fragments
A fragment may outlive its views. It is possible for a fragment to:
- Be created and have
onCreate()
be called - Have
onCreateView()
andonViewCreated()
be called - Have
onDestroyView()
be called - Have
onCreateView()
andonViewCreated()
be called again (e.g., the fragment gets re-displayed) - Have
onDestroyView()
be called - Have
onDestroy()
be called
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
.
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:
- Call
requireBinding()
to get the view binding object… or throw an exception if you called it sometime outside of theonCreateView()
/onDestroyView()
pair - Call
useBinding()
, passing in a lambda expression or other function type, which turns around and invokes your lambda expression with the view binding object
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.