Never directly, and there are three ways to do it properly, in this order of preference. The Fragment Result API for a one off value, a shared ViewModel for state both Fragments care about over time, and an interface on the host Activity, which is the old pattern you should recognise but not reach for.
The Fragment Result API is the right default when one Fragment hands a value back to another and then goes away. The sender calls setFragmentResult() with a request key and a Bundle. The receiver registers setFragmentResultListener() against the same key, and both sides have to be talking to the same FragmentManager. For siblings that is parentFragmentManager, which is what the Fragment level extensions use for you. When a parent listens to a Fragment it hosts, the parent registers on childFragmentManager instead. Register early, in onCreate() or onViewCreated(), because the result is only delivered once the listening Fragment reaches STARTED. Until then the FragmentManager holds it, so a Fragment sitting on the back stack still gets the value when it comes forward. Each result is delivered once and then cleared, and there is one listener and one pending result per key.
// Sender, a bottom sheet handing back the item the user picked
class PickerFragment : DialogFragment() {
private fun onPicked(id: String) {
// The Fragment extension posts to parentFragmentManager
setFragmentResult("pickRequest", bundleOf("itemId" to id))
dismiss()
}
}
// Receiver, registered early so it is listening before the result lands
class ListFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener("pickRequest") { _, bundle ->
val id = bundle.getString("itemId")
}
}
}
A shared ViewModel wins when both Fragments read and write the same state over time, rather than passing one value once. Scope it to the Activity with activityViewModels(), or to a navigation graph so it dies with the flow instead of living as long as the Activity does. Neither Fragment knows the other exists, and the state survives a configuration change.
class SharedViewModel : ViewModel() {
val selected = MutableStateFlow<Item?>(null)
}
class ListFragment : Fragment() {
private val vm: SharedViewModel by activityViewModels()
}
class DetailFragment : Fragment() {
// Scoped to the checkout graph instead, so it is gone when the flow ends
private val vm: SharedViewModel by hiltNavGraphViewModels(R.id.checkout_graph)
}
An interface on the host Activity is the pattern you will still find in older code. The Fragment declares a small callback interface, the Activity implements it, the Fragment casts its context in onAttach(), and calls through it.
class ListFragment : Fragment() {
interface Host { fun onItemPicked(id: String) }
private lateinit var host: Host
override fun onAttach(context: Context) {
super.onAttach(context)
host = context as Host // blows up if this Activity forgot to implement it
}
private fun onPicked(id: String) = host.onItemPicked(id)
}
It is fragile for three reasons. The Fragment is now coupled to one kind of host, so you cannot reuse it in another Activity or nest it inside another Fragment. The cast is unchecked, so a mistake becomes a crash at runtime instead of a compile error. And nothing in it survives recreation, so after a rotation or a process death the Activity has to rebuild the state by hand.
In the room, say Fragment Result API for a one shot value, shared ViewModel for ongoing state, and interfaces only when you are reading a legacy codebase. Then name the two anti patterns, because that is what the interviewer is listening for. Never hold a direct reference to the other Fragment, since it breaks the moment either one is reused and it leaks when the other is detached. And never route it through a static field or an event bus, since you lose all lifecycle safety and nobody reading the code can tell where a value came from.