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 MeasureSpec values (one for width, one for height) combining a mode — EXACTLY (must be this size), AT_MOST (up to this size), or UNSPECIFIED — with a size in pixels. Each child measures itself (and, if it is a ViewGroup, recursively measures its own children first) and reports back its desired size by calling setMeasuredDimension(). 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 View in the tree has a measured size, each ViewGroup assigns each child an exact rectangle — its final position and size — by calling child.layout(left, top, right, bottom), using whatever positioning rule that layout type implements (stacking for LinearLayout, absolute or relative constraints for ConstraintLayout, and so on).

  • Draw. Each View paints itself into a Canvas clipped to the rectangle layout() assigned it, in tree order (a parent draws its own background first, then delegates to dispatchDraw() for its children), so a later sibling drawn on top can overlap an earlier one that shares screen space (as FrameLayout'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.

flowchart TB A["Activity / Fragment inflates layout XML"] --> B["View tree built (ViewGroup with View children)"] B --> C["Measure pass: parent -> children -> parent (bottom-up sizes)"] C --> D["Layout pass: parent -> children (top-down positions)"] D --> E["Draw pass: parent draws, then dispatches to children (tree order)"] E --> F["Pixels on screen"] F -->|"requestLayout()"| C F -->|"invalidate()"| E

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_end above 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_favorite above, each pointing at the next) into one unit whose overall distribution is controlled by layout_constraintHorizontal_chainStyle (or …​_chainStyle on the vertical axis) on the chain’s first element: spread (the default, even gaps — used above), spread_inside (no gap at the outer ends), or packed (views touching, the whole group centered or biased with layout_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 by barrierDirection. header_barrier above sits just below whichever of trail_name (which can wrap to two lines for a long trail name) or difficulty_badge ends up taller, so trail_region never overlaps either one regardless of content length — something a fixed layout_marginTop on trail_region could 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)

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 in onCreate()/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 no findViewById call 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 current Activity/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.