There’s been a long standing footgun when using fragments on Android because the fragment may live longer than it’s views. Solutions to this have ranged from ignoring the problem to some complicated rxjava setup. However, with the additions to AndroidX this is no longer necessary!

The trick is to scope all view interactions to onViewCreated().

class MyFragment : Fragment(R.layout.my_fragment) {
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val binding = MyBinding.bind(view)
    binding.text = "Look, no leaks!"
  }
}

If you don’t assign any views to fragment fields, you don’t need to worry about clearing them out.

Sidenote: if you are not familiar with some of the features here, check out the new fragment constructor and view binding.

Now, you may be wondering how you are supposed to accomplish much with this limited scope, but with a few other AndroidX features it turns out you can do quite a lot.

Listening for updates

The other key part to this is viewLifecycleOwner. This gives you a scope that you can use for asynchronous operations. For example, you can listen to liveData:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  val binding = MyBinding.bind(view)
  viewModel.title.observe(viewLifecycleOwner) { text -> binding.text = text }
}

Or scope a coroutine/Flow operation:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  val binding = MyBinding.bind(view)
  viewLifecycleOwner.lifecycleScope.launchWhenStarted {
    viewModel.title.collect { text -> binding.text = text } 
  }
}

Lifecycle Events

If you need to handle view-related things in other lifecycle events like onPause/onResume, you can attach your own LifecycleObserver.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  val binding = MyBinding.bind(view)
  viewLifecycleOwner.lifecycle.addObserver(object: DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
      Snackbar.make(view, "OnResume Called", Snackbar.LENGTH_LONG).show()
    }
  })
}

Other Callbacks

The one place where this gets more tricky is if you need to handle other callbacks that haven’t yet been broken out by AndroidX. For example, runtime permissions. In these cases, I would recommend routing the event in a way you can react to it.

For example, you could use an event wrapper with live data:

private val showCamera = MutableLiveData<Event<Boolean>>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  val binding = MyBinding.bind(view)

  showCamera.observe(viewLivecycleOwner) { event ->
    event.getContentIfNotHandled()?.let { permission ->
      if (permission) {
        startActivity(Intent(requireContext(), CameraActivity::class.java)
      }
    }
  }

  binding.showCameraButton.setOnClickListener {
    if (ContextCompat.checkSelfPermission(
        requireContext(), 
        Manifest.permission.CAMERA == PackageManager.PERMISSION_GRANTED
    ) {
      showCamera.value = Event(true)
    } else {
      requestPermissions(arrayOf(Manifest.permission.CAMERA), 0)
    }
  }
}

override fun onRequestPermissionsResult(
  requestCode: Int,
  permissions: Array<out String>,
  grantResults: IntArray
) {
  if (requestCode == 0 && grantResults.size == 1) {
    showCamera.value = Event(grantResults[0] == PackageManager.PERMISSION_GRANTED)
  }
}

Or use a Channel:

private val showCamera = Channel<Boolean>(1)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  val binding = MyBinding.bind(view)

  viewLifecycleOwner.lifecycleScope.launchWhenStarted {
    for (permission in showCamera) {
      if (permission) {
        startActivity(Intent(requireContext(), CameraActivity::class.java)
      }
    }
  }

  binding.cameraButton.setOnClickListener {
    if (ContextCompat.checkSelfPermission(
        requireContext(), 
        Manifest.permission.CAMERA == PackageManager.PERMISSION_GRANTED
    ) {
      showCamera.offer(true)
    } else {
      requestPermissions(arrayOf(Manifest.permission.CAMERA), 0)
    }
  }
}

override fun onRequestPermissionsResult(
  requestCode: Int,
  permissions: Array<out String>,
  grantResults: IntArray
) {
  if (requestCode == 0 && grantResults.size == 1) {
    showCamera.offer(grantResults[0] == PackageManager.PERMISSION_GRANTED)
  }
}

By scoping your views to onViewCreated you don’t have to worry about nulling them out our calling them at the wrong time. And AndroidX gives you the tools to do it!