skip to content

How would you build one Flutter codebase so a note-taking app shows a list-detail split on tablets but a single pane with a separate detail view on phones?

level: seniorimportance: should knowfreq 45%

answer

  1. measure the window, not the device
  2. one width breakpoint at the shell
  3. selection state above the branch
  4. survive fold, rotate, split-screen
  5. adaptive also means input and platform

basics

~20 s

Measure the window with MediaQuery.sizeOf or widthOf at the shell, branch on a width breakpoint such as 600, and render either a Row of list and detail or a single pane. Keep the selected note above the branch so resizing preserves it.

solid answer

~50 s

I build **one** shell that measures available width — `MediaQuery.widthOf(context)` at the top, or `LayoutBuilder` if the shell is embedded — and branches on a breakpoint; Flutter's guidance cites 600 logical pixels for switching navigation. Wide: a `Row` with a constrained list pane and an `Expanded` detail pane. Narrow: the list, and the detail shown as its own page. The **selected note id lives above the branch** in `State` or the app's state layer, so when the window changes — rotating, unfolding, entering split-screen — the new layout shows the same note instead of resetting. I never test "is this a tablet"; a phone in split-screen is narrow and a foldable opened flat is wide. Adaptive also covers input and platform: hover and keyboard shortcuts on desktop, `.adaptive` constructors where the look should follow the platform, and capped content widths so text does not stretch across a monitor.

code

dart · 63 lines
dart
import 'package:flutter/material.dart';

class NotesShell extends StatefulWidget {
  const NotesShell({super.key});

  @override
  State<NotesShell> createState() => _NotesShellState();
}

class _NotesShellState extends State<NotesShell> {
  static const double _twoPaneMinWidth = 600;
  String? _selectedId;

  void _select(String? id) => setState(() => _selectedId = id);

  @override
  Widget build(BuildContext context) {
    final bool twoPane = MediaQuery.widthOf(context) >= _twoPaneMinWidth;
    final Widget list = NoteList(selectedId: _selectedId, onSelect: _select);
    final String? id = _selectedId;

    if (twoPane) {
      return Row(
        children: <Widget>[
          SizedBox(width: 320, child: list),
          const VerticalDivider(width: 1),
          Expanded(child: id == null ? const Center(child: Text('Select a note')) : NoteDetail(id: id)),
        ],
      );
    }
    if (id == null) {
      return list;
    }
    return PopScope<Object?>(
      canPop: false,
      onPopInvokedWithResult: (bool didPop, Object? result) {
        if (!didPop) {
          _select(null);
        }
      },
      child: NoteDetail(id: id),
    );
  }
}

class NoteList extends StatelessWidget {
  const NoteList({super.key, required this.selectedId, required this.onSelect});

  final String? selectedId;
  final ValueChanged<String?> onSelect;

  @override
  Widget build(BuildContext context) => const Placeholder();
}

class NoteDetail extends StatelessWidget {
  const NoteDetail({super.key, required this.id});

  final String id;

  @override
  Widget build(BuildContext context) => Text(id);
}

go deeper

for a junior

Know how to read the window width and switch between a Row of two panes and a single pane at a breakpoint.

for a middle

Explain choosing sizeOf or LayoutBuilder for the shell, one set of breakpoints, and why device-type checks fail in split-screen.

for a senior

Show that selection state lives above the branch, handle routes pushed in narrow mode, cover input and platform adaptations, and test across sizes.

for a principal

Defend one adaptive shell over separate phone and tablet screen trees, and set shared breakpoints and pane components for every feature team.

## Responsive and adaptive Flutter's documentation separates two ideas: - **Responsive**: the UI rearranges to *fit* the space — one pane becomes two, a grid gains columns. - **Adaptive**: the UI stays *usable* in the space and on the platform — the right navigation pattern, the right input handling (touch, mouse, keyboard), platform-appropriate controls. A list-detail note app needs both. The layout split is the responsive part; hover states, keyboard shortcuts and platform conventions are the adaptive part. ## Step 1: measure the right thing Decide at the **shell**, the widget just under the app's route that owns both panes: - `MediaQuery.widthOf(context)` (Flutter 3.35+) or `MediaQuery.sizeOf(context).width` when the shell fills the window; - `LayoutBuilder` when the shell may itself be embedded in a smaller area, for example inside a desktop side panel. Do **not** branch on the platform or a device model. The same Android phone can be full-screen, half of a split-screen, or a freeform window; a foldable can be phone-width folded and tablet-width unfolded. ## Step 2: pick breakpoints Flutter's adaptive guidance points to Material's window size classes and uses 600 logical pixels as the point where bottom navigation gives way to a navigation rail. For a note app, a single threshold near that value is usually enough: | Available width | Layout | |---|---| | below 600 | list only; selecting a note shows the detail full-width | | 600 and above | list pane of fixed width plus an `Expanded` detail pane | Keep breakpoint values in one place, such as a small class of constants, so every screen switches at the same widths. ## Step 3: keep state above the branch The most common bug is **losing the selection when the window changes**. If the detail pane owns "which note is open", folding the phone rebuilds a different subtree and that state is gone. Instead: 1. Store `selectedNoteId` in the shell's `State` or in the app's state layer. 2. Wide layout: highlight it in the list and show it in the detail pane. 3. Narrow layout: if it is set, show the detail full-width; otherwise show the list. 4. System back in narrow mode clears the selection rather than leaving the app. If the narrow layout instead **pushes** a detail route, widening the window leaves that route on top of the split view. Either pop it when the layout becomes wide, or render the narrow detail in place, as the example does, so there is nothing to reconcile. ## Step 4: adapt beyond size - **Input.** Add hover highlights, a visible focus indicator and keyboard shortcuts for mouse and keyboard users on desktop and tablets with keyboards. - **Platform.** Where the control should look native, use constructors such as `Switch.adaptive` or `AlertDialog.adaptive`. - **Width caps.** On a wide monitor, constrain the detail text to a readable width instead of letting it gobble all horizontal space. - **No orientation lock.** Locking portrait can letterbox the app on large screens and foldables. ## Testing Run widget tests at several logical sizes — below, at and above each breakpoint — and one that changes size mid-test to prove the selection survives. Try split-screen and a foldable emulator by hand. ## Why one codebase Separate phone and tablet screen trees duplicate logic, drift apart, and still break in split-screen, where a "tablet" is phone-width. One adaptive shell with shared panes keeps a single source of truth and switches only the arrangement.

  • What goes wrong if the narrow layout pushes a detail route and the user then unfolds the device?
    The pushed route stays on top of the navigator, so the newly wide shell renders its split view underneath a full-screen detail page. Either pop the detail route when the layout crosses into two-pane mode, or render the narrow detail inside the shell as state, so the arrangement is derived from width and selection alone.
  • Why not detect tablets with the platform or screen diagonal and load a tablet screen?
    The window, not the device, sets the available space. A tablet in split-screen can be phone-width, a phone in landscape can be wide, and a foldable changes size at runtime. Measuring width handles all of these with one code path and no device list to maintain.
  • How would you prove in tests that the selection survives a resize?
    Pump the shell at a two-pane width, select a note, change the test view's logical size below the breakpoint, pump again and expect the detail for the same note. Repeat in the opposite direction to catch state that lives inside one branch.

saying these in an interview costs you the question

  • Detect a tablet from the platform and load separate tablet screens.
  • Store the selected note inside the detail pane's own State.
  • Show two panes whenever the device is in landscape.
  • Responsive and adaptive both mean only resizing to fit.
  • Locking to portrait avoids having to handle foldables.