In Flutter, how do you render a price line mixing a struck-through old price with a bold new price, and why prefer Text.rich over RichText?
answer
- one paragraph, a tree of spans
- TextSpan children inherit parent style
- TextDecoration.lineThrough
- Text.rich merges DefaultTextStyle
- RichText skips inherited defaults
basics
~10 sBuild a TextSpan tree, each child with its own TextStyle, and pass it to Text.rich. Text.rich merges the ambient DefaultTextStyle, text scaling and selection support, while RichText uses only what you pass it.
solid answer
~40 sMixed styles in one paragraph are a tree of `InlineSpan`s: a root `TextSpan` with `children`, each child inheriting its parent's style and overriding pieces, for example `TextStyle(decoration: TextDecoration.lineThrough)` on the old price and `FontWeight.bold` on the new one. I pass it to `Text.rich`. `Text` and `Text.rich` merge the nearest `DefaultTextStyle` into the root style, apply the ambient text scaler from `MediaQuery`, honour the bold-text accessibility setting and register with a `SelectionArea`; internally they build a `RichText` with all of that resolved. Using `RichText` directly gives none of it: you must pass a complete style, and text does not scale with the user's settings unless you pass a `textScaler`. `WidgetSpan` embeds an icon or badge inline, and a `TextSpan.recognizer` makes a span tappable, with the owning `State` disposing it.
go deeper
Recall that TextSpan children style parts of one paragraph and that Text.rich displays them; know TextDecoration.lineThrough for a struck price.
Explain what Text.rich adds over RichText: DefaultTextStyle merge, text scaling, bold text and selection, plus WidgetSpan and recognizers.
Own recognizer lifecycles, semantics labels for prices, and theme-derived span styles in shared price components.
Define how rich text components expose styling so product teams get consistent, accessible prices without hand-built span trees.
## A paragraph is a tree of spans Flutter lays out a paragraph from a tree of **`InlineSpan`** objects. The common subclass is **`TextSpan`**, which carries `text`, an optional `style` and optional `children`. Each child inherits the resolved style of its parent and overrides only what it sets. So a coupon's price line is one paragraph, not three widgets in a `Row`, and it wraps, aligns and baselines as a unit. ```dart import 'package:flutter/material.dart'; class CouponPrice extends StatelessWidget { const CouponPrice({super.key, required this.oldPrice, required this.newPrice}); final String oldPrice; final String newPrice; @override Widget build(BuildContext context) { final ColorScheme scheme = Theme.of(context).colorScheme; return Text.rich( TextSpan( children: <InlineSpan>[ TextSpan( text: oldPrice, style: TextStyle( decoration: TextDecoration.lineThrough, color: scheme.onSurfaceVariant, ), ), const TextSpan(text: ' '), TextSpan( text: newPrice, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 22, color: scheme.primary, ), ), const TextSpan(text: ' today only'), ], ), maxLines: 2, overflow: TextOverflow.ellipsis, ); } } ``` ## Text.rich versus RichText `Text.rich(span)` is the `Text` widget with a span instead of a string. Its `build` does several things before creating a `RichText`: 1. **Merges `DefaultTextStyle`.** If the root style is null or has `inherit: true` (the default), it is merged onto `DefaultTextStyle.of(context).style`, so the price picks up the font, size and color of the surrounding `Material` or `Card`. 2. **Applies text scaling.** Without an explicit `textScaler`, it uses `MediaQuery.textScalerOf(context)`, so the user's system text size applies. 3. **Applies bold text.** If the platform's bold-text accessibility setting is on, it merges `FontWeight.bold`. 4. **Takes defaults** for `textAlign`, `softWrap`, `overflow` and `maxLines` from `DefaultTextStyle`. 5. **Supports selection** by registering with an enclosing `SelectionArea`. `RichText` is the lower-level widget that does none of this. Its `textScaler` defaults to `TextScaler.noScaling`, its `softWrap` and `overflow` default to `true` and `TextOverflow.clip` regardless of context, and its `text` is used as given, so a `TextSpan` without a full style renders with the engine's bare defaults. | Aspect | `Text.rich` | `RichText` | |---|---|---| | Inherits `DefaultTextStyle` | Yes | No | | Follows system text size | Yes, via `MediaQuery` | Only if you pass `textScaler` | | Bold-text setting | Applied | Not applied | | Works with `SelectionArea` | Yes | Needs a `selectionRegistrar` | Use `RichText` only when you deliberately want to bypass the ambient styling, for example inside a custom painter-like widget with its own fully specified style. ## Beyond text: WidgetSpan and recognizers - **`WidgetSpan`** places a widget inline, such as a coupon's store logo or a small "NEW" badge. Its `alignment` (a `PlaceholderAlignment`, for example `middle` or `baseline`) positions it relative to the text. - **`TextSpan.recognizer`** attaches a `GestureRecognizer`, typically `TapGestureRecognizer`, so "Terms apply" can be tapped. The span does not own the recognizer: the widget that creates it (usually a `State`) must keep it across rebuilds and call `dispose` on it in its own `dispose`. - **`semanticsLabel`** on a `TextSpan` lets a screen reader say "was 20 dollars, now 15 dollars" instead of reading symbols. ## Semantics and selection of rich text A screen reader reads the spans' text in order, so a struck-through price is read as a plain number unless you say otherwise. Setting `semanticsLabel` on the root `TextSpan`, or wrapping the line in `Semantics` with a clear label, lets it announce the old and new prices in words. When the page sits inside a `SelectionArea`, `Text.rich` registers automatically so users can select and copy the price; with `RichText` you would have to pass a `selectionRegistrar` yourself, which is one more reason to prefer `Text.rich` in application code. ## Styling decisions inside spans - Prefer **theme-derived** colors and sizes, for example `Theme.of(context).textTheme.titleLarge` as the base for the new price, so the line follows the app theme. - Keep `inherit: true` on child styles; `inherit: false` cuts the chain and requires a complete style. - `TextDecoration.lineThrough`, `underline` and `overline` can be combined with `TextDecoration.combine`, and `decorationColor` and `decorationThickness` refine them. ## Common mistakes - Building the price line as a `Row` of three `Text` widgets, which breaks wrapping and baseline alignment. - Using `RichText` and then wondering why the text ignores the theme and the user's text size. - Creating a `TapGestureRecognizer` inside `build`, which leaks one per rebuild because nothing disposes it. - Setting `inherit: false` on a child style and losing the font family and size.
- In Flutter, why is creating a TapGestureRecognizer inside build() for a TextSpan a bug?A `TextSpan` does not manage its recognizer's lifecycle; its owner must call `dispose`. Creating one in `build` makes a new, never-disposed recognizer on every rebuild. Create it once in a `State` (for example in `initState`), reuse it in `build`, and dispose it in `dispose`.
- In Flutter, how would you put a small store logo inline at the start of a coupon's price line?Add a `WidgetSpan(alignment: PlaceholderAlignment.middle, child: logo)` as the first child of the root `TextSpan`. The paragraph reserves space for it, wraps around it and aligns it with the text, which a separate `Row` cannot do when the text wraps.
- In Flutter, what does inherit: false on a child TextStyle inside a TextSpan tree do?It stops that span from inheriting the parent span's and `DefaultTextStyle`'s values. Anything the style does not set falls back to engine defaults rather than the app's font, size and color, so it is rarely what you want for inline emphasis.
saying these in an interview costs you the question
- Building mixed-style text as a Row of separate Text widgets
- Believing RichText inherits DefaultTextStyle like Text does
- Creating TapGestureRecognizer objects inside build without disposing them
- Thinking child TextSpans must repeat the parent's full style
- Assuming RichText follows the system text size automatically