An external display is a second window, not a bigger one
Android 16 makes external displays a mainstream concern. The layout work is mostly window size classes you already have; the new parts are density, input capability and deciding what belongs on which screen.

Day 34 of 100, closing the adaptive pillar. Desktop windowing and connected displays moved from a niche concern to a platform feature, and the good news is that most of the work is already done if you followed Day 29.
The symptom
An app that behaves perfectly on a tablet and awkwardly on a monitor:
Button(
onClick = { … },
modifier = Modifier.size(56.dp), // a comfortable thumb target
) { Icon(Icons.Default.Add, null) }
On a 27-inch display driven by a mouse, a 56dp button is enormous and the cursor is precise to a pixel. Meanwhile the text you sized for arm's-length phone reading is small at desk distance, and the two-pane layout that filled a tablet has a third of the screen of empty space in the middle.
Nothing is broken. It just looks like a phone app that was stretched.
Why the obvious fix fails
The obvious fix is another size-class branch:
val size = if (widthClass == EXPANDED) 36.dp else 56.dp
Width is a poor proxy for input method. A tablet in landscape is also EXPANDED and is
still touched with fingers, so this shrinks the buttons on a device where they need to
stay big. And a folded phone connected to a monitor puts your app on a large display
while the phone's window is still compact.
Size tells you about room. It tells you nothing about how the user is pointing at things.

The actual mechanism
Three properties vary independently, and treating them as one is the mistake:
Window size — currentWindowAdaptiveInfo(), unchanged from Day 29. This drives
layout structure, and an external display is simply a large window.
Density — a monitor typically has lower pixel density and greater viewing distance.
LocalDensity reports it; the practical effect is that dp is doing its job and you
mostly shouldn't fight it.
Input capability — the actually-new question:
val config = LocalConfiguration.current
val hasPointer = config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH
val hasKeyboard = config.keyboard == Configuration.KEYBOARD_QWERTY
That's the property worth branching on for target sizing, hover states and keyboard shortcuts — not width.
Compose already handles the interaction side once you use the standard modifiers.
Modifier.clickable provides hover and focus states to any Indication that supports
them, so a mouse gets hover feedback for free. Modifier.hoverable and
onPointerEvent(PointerEventType.Enter) give finer control when a component needs it.
Multi-window is the model
Android 16's desktop windowing means your activity can be resized freely, and connected displays mean it can be moved between screens. Both are configuration changes, both recompose, and both are already handled if size classes drive your layout.
The genuinely new decision is whether your app should present different content on a second display rather than the same content larger. Presentation APIs support that — a slideshow on the projector with notes on the phone, a game on the TV with controls in hand — and it's a product decision rather than a layout one.
For most apps the answer is no: one window, sized by its class, on whichever display it happens to be. That's the default and it's usually right.
What actually needs doing
A short and finite list, which is the reassuring part:
Keyboard navigation. Tab order and Enter/Space activation. Modifier.focusable() and
a sensible focusGroup structure — and this is accessibility work you should be doing
anyway, which is Day 67.
Hover states. Free with clickable, worth checking on components that use a custom
Indication.
Right-click where it means something: Modifier.onPointerEvent filtering for
PointerButton.Secondary.
Text selection. SelectionContainer around content a desktop user will expect to be
selectable. On a phone almost nobody tries; on a monitor everybody does.
Window resize without jank. Your layout recomposes on every resize frame while a window is dragged, so an expensive composition is visible as lag in a way it never was on a phone. Day 7 and Day 10 pay off here.
The thing that breaks
The one genuine trap: cached configuration read once.
// Wrong — captured on first composition, never updated
val isLarge = remember { widthClass == EXPANDED }
On a phone this is nearly harmless, since the value changes only on rotation. Drag a
window across a display boundary and it's wrong for the rest of the session. Read
adaptive info during composition and let recomposition do its job — remember it only
with the value as a key, as in Day 29.
How to prove it
Desktop windowing is testable without hardware: enable it in Developer Options on a Android 16 device or emulator, then drag your app's window across the full range of widths. The layout should change at 600 and 840 and never flicker between.
For input capability, an emulator with a mouse gives you hover and right-click. For keyboard, tab through an entire screen and confirm every interactive element is reachable and activates on Enter — if that fails, it fails for screen-reader users too.
The resize-jank check is the one that finds real problems:
adb shell wm size 1280x800 # then drag the window and watch for dropped frames
If dragging is smooth, composition is cheap. If it stutters, something expensive is running per frame, and it was probably stuttering on scroll too.
The activity flags that still matter
Two manifest details decide whether the platform even lets your app behave well, and both predate Compose:
<activity
android:resizeableActivity="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
resizeableActivity="false" opts you out of multi-window entirely — the system
letterboxes your app rather than resizing it, and no amount of adaptive Compose code runs.
It's a legacy escape hatch and it reads to users as an app that doesn't work on their
device.
configChanges is the more nuanced one. Declaring it avoids activity recreation on
resize, which makes window dragging smoother. It is not a substitute for state that
survives — process death still happens — so treat it as a performance option on top of
correct state handling, never instead of it.
What this generalizes to
The pillar's conclusion: stop asking what device this is. Ask how much room there is, what the user is pointing with, and where the awkward physical regions are. Those three questions cover phones, tablets, foldables, desktops and monitors, and they will cover whatever comes next, because they describe the situation rather than the hardware.
That's the through-line of six days — isTablet() was never really asking about tablets,
it was asking about room, badly.
It is also why the adaptive work compounds rather than accumulating. Size classes bought
the pane scaffolds; the pane scaffolds bought hinge placement; the state discipline that
survives a fold is the same discipline that survives process death. Six days of adaptive
support turns out to be four days of ordinary good practice and two days of genuinely new
API. Tomorrow, Day 35 begins the components pillar with
Scaffold and the app-shell structure everything else sits inside.
Day 34 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Connected displays.