Views, XML Layouts, View Binding and Material Components
|
This content was generated with the assistance of AI and should be verified against the official documentation of Android before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
This page covers the classic View-based UI toolkit: how a screen is built as a tree of View and ViewGroup
objects, how that tree is declared in XML layout resources, how code binds to it safely, and how it is themed
with Material Design Components. It targets the section’s baseline — Android 17 (API 37), AGP 9.4.1,
AndroidX ConstraintLayout 2.2.1, Material Components for Android (MDC) 1.13.0 — and builds on
Resources, Configuration, Localization and Themes and
Fragments and the Fragment Lifecycle. The running example is TrailMate's
classic Views UI: a TrailListFragment showing a scrolling list of trails, and a TrailDetailFragment this
page builds a full layout for.
Jetpack Compose, not the View system, is Google’s recommended UI toolkit for new screens, and TrailMate’s own new screens (introduced on Jetpack Compose Fundamentals) are built with it. This page still covers the View system in depth because most production Android codebases still contain large amounts of it, because View interop remains part of a Compose migration (Migrating from Views to Compose), and because several of its ideas — constraints, insets, theming — reappear under different names in Compose. Kotlin language features used below are not retaught; see Kotlin for Android.
The View and ViewGroup Hierarchy
Every classic Android screen is a tree rooted at a single View. View is the base class for anything drawn
on screen and capable of handling touch input — TextView, Button, ImageView. ViewGroup extends View
and adds the ability to contain and position other View instances as children, which is what makes a tree
possible rather than a flat list: LinearLayout, FrameLayout and ConstraintLayout, covered below, are all
ViewGroup subclasses, and so is RecyclerView
(RecyclerView and Lists). A Fragment's onCreateView() inflates
one such tree from an XML layout resource and returns its root View; the Fragment's host Activity holds
the overall window’s root ViewGroup inside its DecorView.
Three passes turn that tree into pixels on screen, run top-down then bottom-up then top-down again, every time
the tree (or a View's content) changes in a way that could affect its size or position:
-
Measure. The parent tells each child how it may size itself, as a pair of
MeasureSpecvalues (one for width, one for height) combining a mode —EXACTLY(must be this size),AT_MOST(up to this size), orUNSPECIFIED— with a size in pixels. Each child measures itself (and, if it is aViewGroup, recursively measures its own children first) and reports back its desired size by callingsetMeasuredDimension(). This pass is depth-first and bottom-up in its results: a parent cannot know its own size until every child it needs to accommodate has reported one. -
Layout. Now that every
Viewin the tree has a measured size, eachViewGroupassigns each child an exact rectangle — its final position and size — by callingchild.layout(left, top, right, bottom), using whatever positioning rule that layout type implements (stacking forLinearLayout, absolute or relative constraints forConstraintLayout, and so on). -
Draw. Each
Viewpaints itself into aCanvasclipped to the rectanglelayout()assigned it, in tree order (a parent draws its own background first, then delegates todispatchDraw()for its children), so a later sibling drawn on top can overlap an earlier one that shares screen space (asFrameLayout's stacked children below do).
A layout inflated once is not necessarily measured and drawn only once: calling requestLayout() on any View
(triggered internally by, for example, changing a TextView's text to a longer string) schedules a new
measure/layout pass for the whole affected subtree on the next frame, while invalidate() schedules only a
redraw without remeasuring. Understanding which of the two a given change needs is the basis of the custom-view
performance advice on
Custom Views, Touch, Drawing and View Animation. See
Android Developers — View and
Android Developers — ViewGroup.
Declaring Layouts in XML
A layout resource is an XML file under res/layout/, one root ViewGroup per file, inflated at runtime into a
live View tree. TrailMate’s TrailListFragment inflates fragment_trail_list.xml; the rest of this section
builds fragment_trail_detail.xml for TrailDetailFragment, one layout type at a time.
LinearLayout
LinearLayout arranges its children in a single row or column, in declaration order, controlled by
orientation. Its most useful feature is layout_weight: after every child’s fixed (wrap_content/exact)
dimensions are measured, any remaining space along the layout’s orientation is distributed among children in
proportion to their layout_weight, which is how a header/content/footer split with a growing middle section is
built without ConstraintLayout:
<!-- app/src/main/res/layout/item_trail_stat.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingVertical="8dp">
<TextView
android:id="@+id/stat_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/trail_detail_length_label" />
<TextView
android:id="@+id/stat_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold" />
</LinearLayout>
Giving the weighted child 0dp along the weighted axis (rather than wrap_content) lets the layout measure it
in a single pass instead of measuring it twice (once unconstrained, once against the leftover space), which
matters once a screen nests several weighted LinearLayout instances. LinearLayout is the right tool for a
simple, single-axis stack; anything with two-dimensional relationships between children is cheaper to express,
and cheaper to measure, as a single ConstraintLayout (see below) than as nested LinearLayout instances. See
Android Developers — Linear Layout.
FrameLayout
FrameLayout stacks its children on top of one another, each positioned independently (by layout_gravity and
margins) within the parent’s bounds, and draws them in declaration order — later children on top. It is the
right container for exactly one child (a single fragment’s container, android:id="@+id/fragment_container",
as used by NavHostFragment on
Navigation with Fragments) or for a small number of
deliberately overlapping children, such as a favorite-toggle badge drawn over a trail photo:
<!-- fragment of fragment_trail_detail.xml -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="240dp">
<ImageView
android:id="@+id/trail_photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="@null" />
<ImageButton
android:id="@+id/favorite_toggle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="top|end"
android:layout_margin="16dp"
android:background="@android:color/transparent"
android:src="@drawable/ic_favorite_filled"
android:contentDescription="@string/cd_favorite_toggle" />
</FrameLayout>
android:contentDescription="@null" on the photo explicitly marks it as decorative for accessibility services
(the photo’s meaning is already conveyed by the surrounding trail name and stats), while the favorite toggle
carries a real, localized content description since it is an actionable control. See
Android Developers — FrameLayout.
ConstraintLayout: Chains, Guidelines and Barriers
ConstraintLayout positions each child by constraints — rules tying one of its edges to an edge of the
parent or of another sibling — rather than by nesting. Because every child is measured and positioned by one
flat set of constraints instead of by a tree of nested ViewGroup instances, a ConstraintLayout screen
typically has a shallower view hierarchy than the equivalent built from nested LinearLayout/FrameLayout,
which measures faster (fewer measure/layout passes across fewer ViewGroup levels) and is the reason
ConstraintLayout is the default root layout for new Views-based screens.
The full trail-detail screen below (a MaterialToolbar above a NestedScrollView that holds the content)
combines three ConstraintLayout features at once: a guideline for a
fixed margin shared by several children, a chain to distribute three stat labels evenly, and a barrier
so a long trail name never overlaps the difficulty badge next to it regardless of either view’s length:
<!-- app/src/main/res/layout/fragment_trail_detail.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.core.widget.NestedScrollView
android:id="@+id/trail_detail_scroll"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- shared 16dp start/end margin as a reusable vertical guideline pair -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guide_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guide_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<FrameLayout
android:id="@+id/photo_container"
android:layout_width="0dp"
android:layout_height="240dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<!-- the stacked photo and favorite toggle from the FrameLayout example above -->
<ImageView
android:id="@+id/trail_photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="@null" />
<ImageButton
android:id="@+id/favorite_toggle"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="top|end"
android:layout_margin="16dp"
android:background="@android:color/transparent"
android:src="@drawable/ic_favorite_filled"
android:contentDescription="@string/cd_favorite_toggle" />
</FrameLayout>
<TextView
android:id="@+id/trail_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
app:layout_constraintTop_toBottomOf="@id/photo_container"
app:layout_constraintStart_toStartOf="@id/guide_start"
app:layout_constraintEnd_toStartOf="@id/difficulty_badge"
android:layout_marginTop="16dp" />
<com.google.android.material.chip.Chip
android:id="@+id/difficulty_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/trail_name"
app:layout_constraintBottom_toBottomOf="@id/trail_name"
app:layout_constraintEnd_toEndOf="@id/guide_end" />
<!-- barrier on the tallest/widest of name and badge, so region text never overlaps either -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/header_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="trail_name,difficulty_badge" />
<TextView
android:id="@+id/trail_region"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/header_barrier"
app:layout_constraintStart_toStartOf="@id/guide_start"
app:layout_constraintEnd_toEndOf="@id/guide_end"
android:layout_marginTop="4dp" />
<!-- three stats spread evenly with a spread chain -->
<TextView
android:id="@+id/stat_length"
style="@style/TrailMate.TextAppearance.Stat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/trail_region"
app:layout_constraintStart_toStartOf="@id/guide_start"
app:layout_constraintEnd_toStartOf="@id/stat_difficulty"
app:layout_constraintHorizontal_chainStyle="spread"
android:layout_marginTop="16dp" />
<TextView
android:id="@+id/stat_difficulty"
style="@style/TrailMate.TextAppearance.Stat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/stat_length"
app:layout_constraintStart_toEndOf="@id/stat_length"
app:layout_constraintEnd_toStartOf="@id/stat_favorite" />
<TextView
android:id="@+id/stat_favorite"
style="@style/TrailMate.TextAppearance.Stat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/stat_length"
app:layout_constraintStart_toEndOf="@id/stat_difficulty"
app:layout_constraintEnd_toEndOf="@id/guide_end" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/note_input_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/stat_length"
app:layout_constraintStart_toStartOf="@id/guide_start"
app:layout_constraintEnd_toEndOf="@id/guide_end"
android:layout_marginTop="24dp"
android:hint="@string/trail_detail_note_hint">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
-
A guideline (
Guideline) is an invisible line at a fixed position or percentage that other views constrain to instead of the raw parent edge;guide_start/guide_endabove give every child the same 16dp margin from one place, so changing the margin later means editing two lines instead of re-checking every child’s own margin attribute. -
A chain groups views that constrain to each other head-to-tail (
stat_length→stat_difficulty→stat_favoriteabove, each pointing at the next) into one unit whose overall distribution is controlled bylayout_constraintHorizontal_chainStyle(or…_chainStyleon the vertical axis) on the chain’s first element:spread(the default, even gaps — used above),spread_inside(no gap at the outer ends), orpacked(views touching, the whole group centered or biased withlayout_constraintHorizontal_bias). -
A barrier (
Barrier) creates a virtual edge positioned at the furthest edge of a set of referenced views (constraint_referenced_ids), in the direction given bybarrierDirection.header_barrierabove sits just below whichever oftrail_name(which can wrap to two lines for a long trail name) ordifficulty_badgeends up taller, sotrail_regionnever overlaps either one regardless of content length — something a fixedlayout_marginTopontrail_regioncould not guarantee.
0dp width/height (MATCH_CONSTRAINT) on a constrained view — used throughout above — means "fill whatever
space the constraints on both of that view’s opposing edges leave available", the ConstraintLayout equivalent
of LinearLayout's layout_weight="1" with 0dp. See
Android Developers — Build a
Responsive UI with ConstraintLayout and, for chains and barriers specifically, the Chains and Barrier
sections on that same page.
Common Widgets and TextInputLayout
Most of a screen is built from a small set of standard View subclasses: TextView (and its editable
subclass EditText) for text, ImageView for drawables and bitmaps, Button/ImageButton for taps,
CheckBox/RadioButton/Switch (MDC’s SwitchMaterial) for boolean/choice input, and containers such as
ScrollView/NestedScrollView when content can exceed the screen. TextInputLayout, used for TrailMate’s note
field above, wraps an EditText (typically its own TextInputEditText subclass) to add a floating label,
helper/error text, a character counter, and a start/end icon (a password-visibility toggle, a clear button),
all driven from Kotlin without touching the wrapped EditText's own attributes:
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailDetailFragment.kt
binding.noteInputLayout.error =
if (note.length > MAX_NOTE_LENGTH) getString(R.string.trail_detail_note_too_long) else null
binding.noteInputLayout.helperText = getString(R.string.trail_detail_note_helper, note.length, MAX_NOTE_LENGTH)
See Android
Developers — TextInputLayout reference and
Android Developers — Layouts for the
rest of the standard widget catalog.
View Binding
Referencing a View from code used to mean calling findViewById(R.id.trail_name) and casting the result,
once per view, with nothing checking at compile time that the id exists in the layout actually inflated or that
the cast type matches — a renamed or removed id, or a copy-pasted wrong type, only failed at runtime, often as
a ClassCastException or a NullPointerException far from the mistake. View Binding, enabled per module
with buildFeatures { viewBinding = true } in build.gradle.kts, removes both failure modes by generating one
binding class per layout XML file at compile time, named by converting the file name to PascalCase and
appending Binding (fragment_trail_detail.xml → FragmentTrailDetailBinding), with one correctly typed,
non-nullable property per view carrying an android:id:
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailDetailFragment.kt
class TrailDetailFragment : Fragment() {
private var _binding: FragmentTrailDetailBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentTrailDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.trailName.text = trail.name
binding.statDifficulty.text = trail.difficulty.name
binding.favoriteToggle.setOnClickListener { viewModel.onToggleFavorite(trail.id) }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
The nullable backing property plus non-null accessor pattern above (_binding/binding) exists specifically
for Fragment: a Fragment instance can outlive its view (it stays in the FragmentManager's back stack after
onDestroyView() while the Fragment object itself is not destroyed), so holding a strong, non-nullable
reference to the binding past onDestroyView() leaks the whole view tree; clearing _binding there and
crashing loudly on any later access through binding catches that class of bug immediately instead of leaking
memory silently. An Activity, whose view and lifetime coincide, does not need this dance and can hold its
binding as a plain non-nullable lateinit var. See
Android Developers — View Binding.
Data Binding (Legacy) and the History of findViewById
Data Binding, an older and considerably heavier library than View Binding, additionally lets layout XML
itself reference Kotlin/Java expressions (android:text="@{viewModel.trail.name}"), generating an
ObservableField/LiveData-aware binding class that updates the view automatically when the bound data
changes, plus two-way binding (@={}) for input widgets. It requires wrapping the layout root in a <layout>
tag, a <data> block declaring the bound variables, and (for reactive updates) the app’s model classes to
expose LiveData/ObservableField properties — meaningfully more ceremony and compile-time cost than View
Binding for what most screens actually need. It is not the recommended choice for new code: TrailMate exposes
UI state from its ViewModel as a StateFlow collected in Kotlin
(ViewModel, Lifecycle-Aware Components, Coroutines and
Flow), so plain View Binding plus an explicit collect call already gets the "update the view when the state
changes" behavior Data Binding’s expression language exists to provide, without the extra build step, and
without coupling layout XML to Kotlin property names the compiler cannot check as thoroughly as a plain
property reference. Data Binding remains relevant only when maintaining an existing codebase that already uses
it. See Android Developers — Data Binding Library.
Before View Binding existed (introduced in 2020), Android code went through two earlier approaches to this same problem, both worth recognizing when reading older code or books:
-
Plain
findViewById(R.id….), cast by hand at every call site, was the only option from Android’s earliest releases through the mid-2010s — unsafe (no compile-time id/type check) and repetitive (one call per view, often assigned to a field inonCreate()/onViewCreated()). -
Kotlin synthetic properties (
kotlinx.android.synthetic.main.fragment_trail_detail.*, part of the Kotlin Android Extensions Gradle plugin) let code reference a view by its XML id as if it were a top-level property, with nofindViewByIdcall visible at all. Convenient but fragile in ways View Binding fixes: the property existed only if a matching layout happened to be inflated in the currentActivity/Fragment, was easy to reference from the wrong layout by mistake, and was tied to a specific Kotlin compiler plugin. The Kotlin Android Extensions plugin, including synthetics, was deprecated in Kotlin 1.4.20 (2020) and removed entirely — View Binding is its direct, JetBrains- and Google-endorsed replacement, and no supported Kotlin version can compile synthetic imports any more.
Themes vs. Styles
A style is a named bundle of view attributes (textColor, padding, background, and so on) applied to
one View via style="@style/TrailMate.TextAppearance.Stat", as stat_length/stat_difficulty/
stat_favorite do above. A theme is the same underlying <style> XML element, but applied to an entire
Activity or Application (via the manifest’s android:theme) rather than to one view, and read back by
every view in that scope through theme attributes — ?attr/colorPrimary, ?attr/textAppearanceHeadlineSmall
(used on trail_name above) — rather than a literal value, which is what makes swapping the whole theme (light
to dark, brand A to brand B) change every view that referenced it without touching layout XML at all.
<!-- app/src/main/res/values/themes.xml -->
<resources>
<style name="Theme.TrailMate" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/trail_primary</item>
<item name="colorSecondary">@color/trail_secondary</item>
</style>
<style name="TrailMate.TextAppearance.Stat" parent="TextAppearance.Material3.LabelLarge">
<item name="android:textColor">?attr/colorOnSurfaceVariant</item>
</style>
</resources>
A style’s parent attribute establishes inheritance (TrailMate.TextAppearance.Stat starts from Material 3’s
own TextAppearance.Material3.LabelLarge and overrides one attribute), the same mechanism a theme’s own
parent uses to start from a whole Material Design Components base theme instead of defining every attribute
from scratch. See Android Developers — Styles
and Themes.
Material Design Components (MDC)
Material Design Components for Android (com.google.android.material:material, the artifact whose classes
live under com.google.android.material.) is Google’s implementation of the Material Design specification for
the View system: themed, accessible implementations of buttons, chips (Chip, used for difficulty_badge
above), text fields (TextInputLayout), cards (MaterialCardView), navigation components, and more, all
styled consistently from the app’s theme rather than needing per-widget styling. Theme.Material3.DayNight.
(used by Theme.TrailMate above) is MDC’s Material 3 theme family; DayNight in the name means it resolves to
the light or dark variant automatically from the night/notnight qualifier covered on
Resources, Configuration, Localization and Themes, the View
system’s equivalent of Compose’s isSystemInDarkTheme()-driven ColorScheme switch.
DynamicColors
Since Android 12, MDC’s DynamicColors API can derive an app’s whole Material 3 color scheme at runtime from
the user’s wallpaper-based system palette (Material You), instead of the fixed colorPrimary/colorSecondary
values declared in themes.xml:
// app/src/main/kotlin/com/example/trailmate/TrailMateApplication.kt
class TrailMateApplication : Application() {
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
}
}
applyToActivitiesIfAvailable registers an ActivityLifecycleCallbacks that applies dynamic color to every
Activity as it is created, falling back silently to the theme’s own static colors on API levels below 12 or
when dynamic color is otherwise unavailable, so the call is safe to make unconditionally. See
Android Developers — Apply Dynamic
Color and Android Developers — Dark Theme
for AppCompatDelegate.setDefaultNightMode(), the View system’s programmatic light/dark override (already
introduced on Resources, Configuration, Localization and
Themes).
Menus and the App Bar
A menu resource (res/menu/menu_trail_detail.xml) declares a set of <item> elements — id, title, icon,
showAsAction — inflated into an options menu, a contextual action mode, or a popup/overflow menu:
<!-- app/src/main/res/menu/menu_trail_detail.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_share"
android:title="@string/action_share"
android:icon="@drawable/ic_share"
app:showAsAction="ifRoom" />
</menu>
The app bar — com.google.android.material.appbar.MaterialToolbar, MDC’s themed replacement for the
platform Toolbar — is set as the Activity's action bar via setSupportActionBar(), after which it hosts
the inflated menu, the screen title, and an up/back affordance automatically wired to the fragment back stack
when combined with the Navigation component
(Navigation with Fragments):
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailDetailFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
binding.toolbar.inflateMenu(R.menu.menu_trail_detail)
binding.toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_share -> { shareTrail(trail); true }
else -> false
}
}
}
Edge-to-Edge and Window Insets
Since targetSdk 35, edge-to-edge is enforced (introduced on
Getting Started with Android): the app’s window draws behind the status
bar and navigation bar by default, and it becomes the app’s own responsibility to keep interactive content
(a toolbar, a floating action button, the bottom of a scrolling list) from being obscured by those system bars.
ViewCompat.setOnApplyWindowInsetsListener() is how a View-based screen reads how much space the system bars
(and, on some devices, a display cutout) currently occupy, and applies exactly that much as padding or margin to
the views that need it:
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailDetailFragment.kt
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { toolbar, windowInsets ->
val bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
toolbar.updatePadding(top = bars.top)
windowInsets
}
ViewCompat.setOnApplyWindowInsetsListener(binding.trailDetailScroll) { scrollView, windowInsets ->
val bars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
scrollView.updatePadding(bottom = bars.bottom)
windowInsets
}
WindowInsetsCompat.Type.systemBars() covers both the status bar and the navigation bar in one call (other
types exist for the IME/keyboard, a display cutout, and captions); getInsets() returns the current padding
needed on each of the four edges. The listener runs again automatically whenever those insets change — rotating the device, or the 3-button navigation bar hiding/showing — so, unlike a one-time padding value read
in onCreate(), this keeps working across the configuration changes described on
Resources, Configuration, Localization and Themes. Both
listeners return the untouched windowInsets rather than WindowInsetsCompat.CONSUMED, and each applies only
the edge it owns (the toolbar the top, the scroll view the bottom). This matters on TrailMate’s minSdk 26:
before API 30, a view that consumes insets stops them from being dispatched to its later siblings, so
returning CONSUMED from the toolbar would leave trail_detail_scroll without any insets and the end of the
content under the navigation bar. Calling ViewGroupCompat.installCompatInsetsDispatch(binding.root) once
backports the API 30+ dispatch behavior to older versions, if a screen does need to consume insets. Compose’s
own insets handling, built on the same underlying WindowInsetsCompat API, is covered on
Adaptive Layouts, Edge-to-Edge and Large
Screens. See Android Developers — Display Content Edge-to-Edge and
Android Developers — WindowInsetsCompat reference and
Android Developers — ViewGroupCompat reference.
Loading the trail’s photo into the ImageView inside photo_container above — with Coil or Glide, caching,
placeholders, and transformations — is covered separately in
Image Loading, Caching and Transformations with Coil and
Glide, later in this section.