Fragments
A Fragment is a reusable, self-contained piece of UI that lives inside an Activity rather than standing on its own. It has its own layout, its own logic, and — as you'll see below — its own small lifecycle, but it can never appear on screen without a host Activity to hold it.
Why Reach for a Fragment at All?
If an Activity can already show a layout, why add another layer? Two reasons come up constantly in real apps:
- Adapting across screen sizes. On a phone, TrailLog might show the trail list full-screen; on a tablet, it could show the list and a selected trail's detail side by side. Splitting each piece into its own Fragment makes that kind of layout swap realistic without duplicating logic.
- Breaking up a complex screen. A single Activity juggling a map, a trail list, and a filter panel all at once gets messy fast. Three focused Fragments, each responsible for one piece, are far easier to reason about and reuse.
Worked Example: TrailListFragment
Instead of putting the trail list directly in an Activity, TrailLog defines a
TrailListFragment that owns that one job: show the list of logged trails. A Fragment
subclasses Fragment and overrides onCreateView() to inflate its own
layout, separate from whatever layout its host Activity uses.
class TrailListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_trail_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// safe to look up views and bind the trail list adapter here
}
}
Notice that onCreateView() only inflates the layout and returns it — it deliberately
doesn't touch any views yet. That's because the view hierarchy isn't fully ready until
onViewCreated() runs immediately afterward, which is the right place to bind an
adapter or look up individual views.
A Fragment's Own Mini-Lifecycle
A Fragment is attached to a host Activity, so it rides along with that Activity's lifecycle, but it also has extra callbacks of its own that track the Fragment's relationship to its host and its view:
onDestroyView() deserves a second look: the Fragment object can outlive its view, for
example when it's placed on the back stack. Any reference to a view or a View Binding instance
should be cleared here, or you risk holding on to a view that no longer exists.
Hosting a Fragment with FragmentContainerView
A Fragment needs somewhere to live. The modern way to declare that spot in an Activity's XML
layout is FragmentContainerView:
<androidx.fragment.app.FragmentContainerView
android:id="@+id/trail_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.codingwithsonu.traillog.TrailListFragment" />
With android:name set directly on the container, the system instantiates
TrailListFragment and places it there automatically as soon as the Activity's layout
is inflated — no extra Kotlin code required for this simple, static case.
Adding Fragments Programmatically: FragmentTransaction
When a Fragment needs to be swapped in at runtime — say, replacing the trail list with a detail
view when the user taps an item — you reach for a FragmentTransaction, obtained from
a FragmentManager:
childFragmentManager.beginTransaction()
.replace(R.id.trail_fragment_container, TrailDetailFragment())
.addToBackStack(null)
.commit()
childFragmentManager is used when one Fragment hosts another Fragment inside itself;
an Activity hosting a top-level Fragment instead uses supportFragmentManager. This
API is flexible but entirely manual — you're responsible for the back stack, arguments, and
transaction timing yourself. The next lesson,
Navigation Component, covers how the Navigation
library handles most of this automatically in a modern app, so you rarely write raw
FragmentTransaction code by hand once it's set up.
Passing Data Into a Fragment: The newInstance() Pattern
A Fragment needs a no-argument constructor, since the system may recreate it on its own (say,
after a configuration change) without knowing what arguments you'd originally passed. The
established Kotlin idiom for supplying startup data anyway is a companion-object factory function
conventionally called newInstance(), which packages the data into the Fragment's
arguments Bundle:
class TrailDetailFragment : Fragment() {
companion object {
private const val ARG_TRAIL_ID = "trail_id"
fun newInstance(trailId: Int): TrailDetailFragment {
val fragment = TrailDetailFragment()
fragment.arguments = Bundle().apply {
putInt(ARG_TRAIL_ID, trailId)
}
return fragment
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val trailId = arguments?.getInt(ARG_TRAIL_ID) ?: -1
// use trailId to look up and render the right Trail
return inflater.inflate(R.layout.fragment_trail_detail, container, false)
}
}
Callers create the Fragment through TrailDetailFragment.newInstance(trail.id) instead
of the plain constructor, which keeps the "how do I configure this Fragment's starting data"
question answered in exactly one place.