In Flutter, what replaced textScaleFactor, and how should a widget read and apply the user's font size setting?
answer
- a strategy, not a single number
- MediaQuery.textScalerOf(context)
- scaler.scale(fontSize), not multiplication
- Android 14 scales large text less
- TextPainter defaults to noScaling
basics
~10 sTextScaler replaced the double textScaleFactor, because Android 14 scales large text less than small text. Text applies MediaQuery.textScalerOf(context) automatically; custom code calls scaler.scale(fontSize) instead of multiplying by a factor.
solid answer
~40 s`textScaleFactor` assumed scaling was proportional: `fontSize * factor`. Android 14 introduced **nonlinear** font scaling, where large text grows less than small text, which one number cannot express, so Flutter replaced it with `TextScaler`, an object with `scale(double fontSize)`. Read it with `MediaQuery.textScalerOf(context)`; `Text`, `RichText` and the material widgets already apply it. Where you size text yourself — a `TextPainter` in a `CustomPainter`, a box sized to fit a label — call `textScaler.scale(14)` rather than multiplying, and pass `textScaler:` to `TextPainter`, whose default is `TextScaler.noScaling`. Use `TextScaler.linear(1.5)` for a fixed proportional scale and `TextScaler.noScaling` to opt out. `textScaleFactor` on `MediaQueryData`, `Text` and `MediaQuery.textScaleFactorOf` has been deprecated since Flutter 3.16.
code
dart · 26 linesimport 'package:flutter/widgets.dart';
class PlatformBadgePainter extends CustomPainter {
PlatformBadgePainter({required this.platform, required this.textScaler});
final String platform;
final TextScaler textScaler; // from MediaQuery.textScalerOf(context)
@override
void paint(Canvas canvas, Size size) {
final TextPainter painter = TextPainter(
text: TextSpan(text: platform, style: const TextStyle(fontSize: 16)),
textDirection: TextDirection.ltr,
textScaler: textScaler, // defaults to TextScaler.noScaling otherwise
)..layout(maxWidth: size.width);
painter.paint(canvas, Offset.zero);
painter.dispose();
}
@override
bool shouldRepaint(PlatformBadgePainter oldDelegate) =>
oldDelegate.platform != platform || oldDelegate.textScaler != textScaler;
}
// In build(): CustomPaint(painter: PlatformBadgePainter(
// platform: '4', textScaler: MediaQuery.textScalerOf(context)))go deeper
Recall that TextScaler replaced textScaleFactor, that Text applies it for you, and that MediaQuery.textScalerOf(context) reads it.
Explain why a single factor cannot model Android 14's nonlinear curve, and where manual code such as TextPainter must call scale().
Migrate custom painters and text-sized boxes to scale(), and verify screens at the largest font size instead of trusting the default.
Plan removal of deprecated scaling APIs across shared packages before Flutter deletes them, and decide who owns large-text quality.
## From a number to a strategy Users choose a font size in the system accessibility settings, and apps are expected to honour it. Flutter used to expose that setting as a single `double`, **`textScaleFactor`**, and every widget computed `scaledSize = textScaleFactor * fontSize`. A factor of 2.0 turned 14-pixel text into 28. **Android 14** changed the model with **nonlinear font scaling**: at the same setting, small text grows a lot and already-large text grows less, so headlines do not explode off the screen. A single multiplier cannot describe "14 becomes 28 but 32 becomes 44". Flutter therefore introduced **`TextScaler`**, an abstract class whose job is one method: - `double scale(double fontSize)` — the scaled size, in logical pixels, for an unscaled font size. The platform's scaler, `SystemTextScaler`, forwards to the engine's `scaleFontSize`, which implements the nonlinear curve on Android 14 and later. ## Reading and applying it | Deprecated (since 3.16) | Current | |---|---| | `MediaQuery.textScaleFactorOf(context)` | `MediaQuery.textScalerOf(context)` | | `MediaQueryData.textScaleFactor` | `MediaQueryData.textScaler` | | `Text(..., textScaleFactor: 1.2)` | `Text(..., textScaler: TextScaler.linear(1.2))` | | `fontSize * textScaleFactor` | `textScaler.scale(fontSize)` | | `TextScaler.textScaleFactor` getter | `scale()`; the getter is an estimate kept for migration | Most code never touches any of this: `Text`, `RichText`, `EditableText` and the material and cupertino widgets read the ambient scaler from `MediaQuery` and apply it. The places that need attention: 1. **`TextPainter`** — its `textScaler` defaults to `TextScaler.noScaling`, so text drawn in a `CustomPainter` ignores the user's setting unless you pass `MediaQuery.textScalerOf(context)` in. 2. **Boxes sized from text** — a badge whose height is `16 * factor` should use `scaler.scale(16)` instead, so it tracks the nonlinear curve. 3. **Explicit overrides** — `TextScaler.linear(x)` for a proportional scale, `TextScaler.noScaling` to switch scaling off for a subtree (for example a logo rendered as text). ## Non-font dimensions The migration guide warns that when `textScaleFactor` was used to scale things that are **not** font sizes — an icon, a chat bubble's width — there is no generic rewrite: a nonlinear curve does not say how a box should grow. Options: - derive the size from the scaled text itself, via `scaler.scale(fontSize)` or by laying the text out with `TextPainter` and measuring it; - let layout decide: `IntrinsicHeight`, `Flexible` and wrapping widgets size to their scaled content without any arithmetic. ## Version history - **Flutter 3.16:** `textScaleFactor` deprecated in favour of `TextScaler`, and Android 14 nonlinear scaling enabled; the annotations read "after v3.12.0-2.0.pre". - The deprecated members still exist in 3.47 for migration; the docs say they will be removed in a future release, and the migration guide's temporary "force linear scaling" snippet depends on them. ## Pitfalls - Reading `MediaQuery.of(context).textScaler` rebuilds on **every** `MediaQuery` change; `MediaQuery.textScalerOf(context)` depends only on the text scaler aspect. - Passing both `textScaleFactor` and `textScaler` to `Text` fails an assertion; mixing them on `MediaQueryData` does too unless the factor is 1.0. - Testing only at the default size hides everything above; check the largest setting on a real device and in tests.
- Why can't Flutter keep textScaleFactor as a single number on Android 14?Android 14's nonlinear scaling grows small text more than large text at the same setting, so the ratio between scaled and unscaled size depends on the font size. A single factor can only describe proportional scaling. `TextScaler.scale(fontSize)` takes the size as input and can return any curve the platform defines.
- In Flutter, why prefer MediaQuery.textScalerOf(context) over MediaQuery.of(context).textScaler?`MediaQuery.of` makes the widget depend on the whole `MediaQueryData`, so it rebuilds when insets, orientation or any other field changes, such as while the keyboard animates. `textScalerOf` registers a dependency on the text scaler aspect only, so the widget rebuilds just when the font setting changes.
saying these in an interview costs you the question
- textScaleFactor is still the current API for reading the user's font size
- Multiplying fontSize by a factor matches Android 14's scaling
- TextPainter applies the system font size automatically
- Text widgets need manual scaling code to honour the setting
- TextScaler.noScaling is the right app-wide default for stable layouts