skip to content

A Flutter train-timetable screen overflows at the largest system font size; how do you reproduce, find and fix the breakages?

level: seniorimportance: should knowfreq 40%

answer

  1. yellow-and-black overflow stripes
  2. textScaleFactorTestValue in widget tests
  3. fixed heights are the usual culprit
  4. Flexible text, Wrap, then Column
  5. scroll the screen, cap only display text

basics

~20 s

Reproduce at the largest font on devices and at a 2.0 test text scale, find the RenderFlex overflows, then drop fixed heights, let text wrap inside Expanded, stack rows into columns when needed, and make the screen scroll.

solid answer

~40 s

Set the device font to its largest size, and in widget tests set `tester.platformDispatcher.textScaleFactorTestValue = 2.0` (or wrap the screen in a `MediaQuery` with `TextScaler.linear(2.0)`). Debug builds paint yellow-and-black stripes and log 'A RenderFlex overflowed by N pixels', which name the culprits. The usual causes on a timetable row are fixed heights (`SizedBox(height: 56)`), a `Row` whose `Text` children are not wrapped in `Expanded`/`Flexible`, `maxLines: 1` on essential text, and a non-scrolling page. Fix by letting rows size to content, giving each text a flexible share so it wraps, moving side-by-side chips into a `Wrap` or `OverflowBar`, and switching the row to a `Column` when `MediaQuery.textScalerOf(context).scale(16)` passes a threshold. Put the page in a scroll view. Reserve ellipsis for secondary text and a local clamp for display-size text only.

code

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

class DepartureRow extends StatelessWidget {
  const DepartureRow({super.key, required this.time, required this.destination, required this.status});

  final String time;
  final String destination;
  final String status;

  @override
  Widget build(BuildContext context) {
    final bool stacked = MediaQuery.textScalerOf(context).scale(16) > 24;
    final Widget destinationText = Text(destination); // wraps freely
    final Widget statusChip = Chip(label: Text(status));

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: stacked
          ? Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[Text(time), destinationText, statusChip],
            )
          : Row(
              children: <Widget>[
                Text(time),
                const SizedBox(width: 12),
                Expanded(child: destinationText),
                statusChip,
              ],
            ),
    );
  }
}

go deeper

for a junior

Recall that large system fonts break fixed-height rows and unwrapped text, and that debug builds show overflow stripes.

for a middle

Explain how Expanded, Flexible, Wrap and scrolling let text grow, and how to run a widget test at a 2.0 text scale.

for a senior

Diagnose a real screen at the largest setting, switch arrangements based on scaled size, and keep a 2x regression test per key screen.

for a principal

Make large-text behaviour a release criterion for list rows and cards, with budget for layout rework rather than global clamps.

## Reproducing the breakage Large-text bugs hide at the default size, so reproduce them deliberately: - **Devices.** Raise the system font size to the maximum. Android 14 and later scale up to 200% with nonlinear scaling; iOS offers larger accessibility sizes, enabled separately. Check both, because the curves differ. - **Widget tests.** `tester.platformDispatcher.textScaleFactorTestValue = 2.0` sets the scale the test app sees; call `addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue)`. Alternatively wrap the screen under test in `MediaQuery(data: MediaQueryData(textScaler: TextScaler.linear(2.0)), child: ...)`. - **Several screen sizes.** A layout that survives 2x on a tablet can still break on a narrow phone, so combine large text with a small `tester.view.physicalSize`. ## Finding the culprits In debug builds, a `Flex` (`Row` or `Column`) whose children do not fit paints **yellow-and-black stripes** on the overflowing edge and logs **"A RenderFlex overflowed by N pixels on the right"** with the widget's creation location. A widget test fails on the same error, which turns a large-text check into a regression test. Clipped text is subtler: text cut off by a fixed-height parent or `maxLines: 1` produces no error, so screenshots at 2x are worth reviewing. ## Typical causes on a timetable row A departure row usually holds a time, a destination, a platform badge and a status chip ("On time", "Delayed 12 min"). The common failures: | Cause | Symptom at 2x | Fix | |---|---|---| | `SizedBox(height: 56)` around the row | text clipped at the bottom | drop the height; use padding and `ConstrainedBox(minHeight: ...)` | | `Text` in a `Row` without `Expanded`/`Flexible` | RenderFlex overflow to the right | give the destination `Expanded` so it wraps | | `maxLines: 1` on the destination | "London King's Cr..." | allow wrapping; keep ellipsis for secondary text | | chips side by side in a `Row` | overflow or squashed chips | `Wrap` or `OverflowBar` | | the page is a plain `Column` | bottom rows unreachable | a `ListView` or `SingleChildScrollView` | ## Adapting the layout, not the text When scaled text no longer fits side by side, change the **arrangement**: 1. Measure the effective size: `final double scaled = MediaQuery.textScalerOf(context).scale(16);`. 2. Past a threshold, stack the time above the destination with a `Column` instead of a `Row`. 3. Or decide by space: a `LayoutBuilder` compares the available width with what the row needs. Icons usually stay fixed, but touch targets must not shrink as text grows, and the platform badge can grow with `scale()` so it still encloses its number. ## What not to do - **`FittedBox` around body text.** It scales the text back down to fit, which undoes the user's setting while looking like a fix. - **An app-wide clamp.** A root `MediaQuery.withClampedTextScaling` hides every overflow by denying users the size they chose. - **Ellipsis on essential content.** A truncated destination or delay reason loses the information the screen exists to show. A local `maxScaleFactor` on the 48-pixel departure clock is fine; the destination, calling points and delay notices must grow. ## Keeping it fixed - Add a widget test per key screen at 2.0 that pumps the screen and asserts no exceptions, so an overflow fails CI. - Include a large-text screenshot in design review for new rows and cards. - Treat "works at 200%" as part of the definition of done for list rows, which break most often.

  • Why is wrapping body text in FittedBox a bad fix for large-text overflow in Flutter?
    `FittedBox` scales its child down until it fits the available box, so a destination that the user enlarged to 2x is drawn back at the size that fits the old layout. The overflow disappears but so does the user's setting. Fix the layout so the text has room: wrapping, flexible rows, a stacked arrangement, scrolling.
  • In a Flutter widget test, how do you run a screen at twice the default text size?
    Set `tester.platformDispatcher.textScaleFactorTestValue = 2.0` before pumping and register `addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue)`, or wrap the widget in a `MediaQuery` whose data has `textScaler: TextScaler.linear(2.0)`. An overflow then throws during layout and fails the test, making large text a regression check.

saying these in an interview costs you the question

  • Wrapping body text in FittedBox is a clean way to fix overflow
  • Clamping the text scale at the app root solves large-text bugs
  • If there is no overflow error, text at 2x is displayed correctly
  • Fixed row heights are fine as long as they are generous
  • Android and iOS scale text by the same curve