Fragments, the Navigation component and passing arguments
Manage fragment view lifecycle correctly, move between screens with NavController, and pass data without crashes.
Most Android apps today are one activity with many fragments, switched by the Navigation component. Screenings ask what happens to a fragment's view on rotation, how to send an ID to the next screen, and how to get a result back. The answers follow from a few lifecycle facts.
The fragment view has its own lifecycle
A fragment can outlive its view. When it goes on the back stack, onDestroyView() runs but the fragment object stays. Any reference to the view, including a view binding, must be cleared there, and anything observing LiveData or Flow must use viewLifecycleOwner, not this.
class JobListFragment : Fragment(R.layout.fragment_job_list) {
private var binding: FragmentJobListBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = FragmentJobListBinding.bind(view)
binding?.openButton?.setOnClickListener {
val args = bundleOf("jobId" to 42)
findNavController().navigate(R.id.action_list_to_detail, args)
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
}Passing the layout ID to the Fragment constructor removes the need to override onCreateView. The detail fragment reads the argument with requireArguments().getInt("jobId").
Navigating with a graph
The navigation graph XML lists destinations and the actions between them. navigate(actionId) moves forward and adds to the back stack; popBackStack() goes back one; navigate(actionId, args, navOptions { popUpTo(R.id.loginFragment) { inclusive = true } }) removes screens you should not return to, such as login after a successful sign-in. With the Safe Args plugin the call becomes typed and the argument is checked at compile time:
val action = JobListFragmentDirections.actionListToDetail(jobId = 42)
findNavController().navigate(action)
// in JobDetailFragment
private val args: JobDetailFragmentArgs by navArgs()
val id = args.jobIdGetting data back and sharing state
Two tools cover almost every case. For a one-off result, such as a picker returning a choice, use the Fragment Result API. For state several screens read, use a ViewModel scoped to the activity or to the navigation graph.
// picker fragment
setFragmentResult("pickLocation", bundleOf("city" to "Chennai"))
findNavController().popBackStack()
// receiving fragment, in onViewCreated
setFragmentResultListener("pickLocation") { _, bundle ->
binding?.cityText?.text = bundle.getString("city")
}
// shared state
private val shared: FilterViewModel by activityViewModels()Common mistakes and a practice habit
- Observing with
thisas the lifecycle owner, which leaks and fires callbacks on a destroyed view. - Keeping the binding after
onDestroyView, which holds the old view in memory. - Calling
navigatetwice from a double tap, crashing with an unknown action error. Check the current destination first or disable the button. - Using the activity's
supportFragmentManagerinside a fragment instead ofchildFragmentManagerfor nested fragments.
Practice habit: build a two-screen app with a list and a detail, pass an ID forward with Safe Args, return a chosen value with the Result API, then rotate the device on each screen and confirm nothing is lost.
More in Web and app