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()
.
1linkclass MyFragment : Fragment(R.layout.my_fragment) {
2link override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
3link val binding = MyBinding.bind(view)
4link binding.text = "Look, no leaks!"
5link }
6link}
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.
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:
1linkoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {
2link val binding = MyBinding.bind(view)
3link viewModel.title.observe(viewLifecycleOwner) { text -> binding.text = text }
4link}
Or scope a coroutine/Flow operation:
1linkoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {
2link val binding = MyBinding.bind(view)
3link viewLifecycleOwner.lifecycleScope.launchWhenStarted {
4link viewModel.title.collect { text -> binding.text = text }
5link }
6link}
If you need to handle view-related things in other lifecycle events like
onPause/onResume
, you can attach your own LifecycleObserver
.
1linkoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {
2link val binding = MyBinding.bind(view)
3link viewLifecycleOwner.lifecycle.addObserver(object: DefaultLifecycleObserver {
4link override fun onResume(owner: LifecycleOwner) {
5link Snackbar.make(view, "OnResume Called", Snackbar.LENGTH_LONG).show()
6link }
7link })
8link}
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:
1linkprivate val showCamera = MutableLiveData<Event<Boolean>>()
2link
3linkoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {
4link val binding = MyBinding.bind(view)
5link
6link showCamera.observe(viewLivecycleOwner) { event ->
7link event.getContentIfNotHandled()?.let { permission ->
8link if (permission) {
9link startActivity(Intent(requireContext(), CameraActivity::class.java)
10link }
11link }
12link }
13link
14link binding.showCameraButton.setOnClickListener {
15link if (ContextCompat.checkSelfPermission(
16link requireContext(),
17link Manifest.permission.CAMERA == PackageManager.PERMISSION_GRANTED
18link ) {
19link showCamera.value = Event(true)
20link } else {
21link requestPermissions(arrayOf(Manifest.permission.CAMERA), 0)
22link }
23link }
24link}
25link
26linkoverride fun onRequestPermissionsResult(
27link requestCode: Int,
28link permissions: Array<out String>,
29link grantResults: IntArray
30link) {
31link if (requestCode == 0 && grantResults.size == 1) {
32link showCamera.value = Event(grantResults[0] == PackageManager.PERMISSION_GRANTED)
33link }
34link}
Or use a Channel:
1linkprivate val showCamera = Channel<Boolean>(1)
2link
3linkoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) {
4link val binding = MyBinding.bind(view)
5link
6link viewLifecycleOwner.lifecycleScope.launchWhenStarted {
7link for (permission in showCamera) {
8link if (permission) {
9link startActivity(Intent(requireContext(), CameraActivity::class.java)
10link }
11link }
12link }
13link
14link binding.cameraButton.setOnClickListener {
15link if (ContextCompat.checkSelfPermission(
16link requireContext(),
17link Manifest.permission.CAMERA == PackageManager.PERMISSION_GRANTED
18link ) {
19link showCamera.offer(true)
20link } else {
21link requestPermissions(arrayOf(Manifest.permission.CAMERA), 0)
22link }
23link }
24link}
25link
26linkoverride fun onRequestPermissionsResult(
27link requestCode: Int,
28link permissions: Array<out String>,
29link grantResults: IntArray
30link) {
31link if (requestCode == 0 && grantResults.size == 1) {
32link showCamera.offer(grantResults[0] == PackageManager.PERMISSION_GRANTED)
33link }
34link}
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!