How to avoid device text size settings affecting your Flutter layout

Yuichi Fujiki
8 min readApr 23, 2023
Photo by Mel Poole on Unsplash

Both iOS and Android devices have accessibility settings that allow users to change devices’ text sizes. That is a great thing as a user, especially feeling age myself 😭. But as a mobile developer, that could sometimes be a pain, especially in the Flutter environment, where these settings are respected by default. Again, accessibility is a great thing and if you want to go by the book, you should respect it and think of a layout design that wouldn’t break even with the users’ large text settings. But unfortunately, there could be numerous reasons you might want to opt out of this in the actual development environment, e.g.,

  • The project does not have enough budget to design/develop all those cases
  • The target demography of the project does not require that, at least for the beginning
  • Your designer wants to keep the exact text size that he/she designed perfectly along with other screen elements
  • etc etc…

So, if your situation applies to some of the above and is crucial for your project, this article is for you. I will try to explain how to opt out of this default setup in Flutter.

The issue

I have created a sample code with various controls. I will explain how these controls are categorized later, but

  • The first tab includes Text. SelectableText, Tooltip
  • The second tab includes TextButton, ElevatedButton, ListTile,
  • The third tab includes Tab
  • The fourth tab includes TextField, TextFormField
  • The fifth tab includes RichText

It looks a quite normal sample app :

However, let’s try to change the text size in the settings.

You can change the text size from your settings

This example is on iOS, but Android has similar settings too.

Now, reopen the sample app again:

You can see that this large text setting has messed up the layout quite a bit. In some situations, you can not even see the entire text because it became too wide.

However, note that only the RichText in the fifth tab was not affected by the user’s text size settings. That is why I separated that control in the tab called “Exception”. For RichText widgets, you don’t really need to do anything if you want to keep the constant size. (Rather, you would need to do something special if you want your app to comply with users’ text size settings)

Now, let me explain the implementation by each tab.

In the first tab, I put the widgets that specify string directly as the parameter. Look at the implementation:

Text("Text widget", style: TextStyle(fontSize: 16)),
...
SelectableText("SelectableText widget",
style: TextStyle(fontSize: 24)),
...
Tooltip(
height: 100,
message: "Tooltip",
child: Text(
"I show a tooltip, long press me",
style: TextStyle(
fontSize: 16,
color: Colors.blue,
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.solid,
),
),
),

Text, SelectableText, Tooltip each takes a string literal directly as their parameters. For these controls, you have to create your own custom control to opt out of Flutter’s default behavior. Creating custom control may sound scary, but it is actually pretty simple. Let’s see the example of MyText widget implementation.

Text, Selectable widget case

import 'package:flutter/material.dart';

class MyText extends StatelessWidget {
final String data;
final TextStyle? style;
final StrutStyle? strutStyle;
final TextAlign? textAlign;
final TextDirection? textDirection;
final Locale? locale;
final bool? softWrap;
final TextOverflow? overflow;
final int? maxLines;
final String? semanticsLabel;
final TextWidthBasis? textWidthBasis;
final TextHeightBehavior? textHeightBehavior;

const MyText(
this.data, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
});

@override
Widget build(BuildContext context) {
return Text(data,
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaleFactor: 1.0, // THIS IS THE ONLY IMPORTANT PART //
maxLines: maxLines,
semanticsLabel: semanticsLabel,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior);
}
}

It may still look scary at a glance :) but it’s actually just a stateless widget that copies all signatures from Text widget. All the parameter values that are given to MyText widget will be passed down to the child Text widget, except fortextScaleFactor field. textScaleFactor field is always kept1 which would avoid the text to scale according to the device settings.

The rest is easy. You just replace Text widget with MyText widget and that takes care of it. SelectedText is similar. It has textScaleFactor parameter too, so you can specify that as 1 in your MySelectedText implementation.

The tooltip case

Tooltip is a little bit different since it doesn’t have textScaleFactor parameter. We need to calculate the font size ourselves and specify that in textStyle parameter. Let’s see the implementation:

import 'package:flutter/material.dart';

class MyTooltip extends StatelessWidget {
final String? message;
final InlineSpan? richMessage;
final double? height;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
final double? verticalOffset;
final bool? preferBelow;
final bool? excludeFromSemantics;
final Decoration? decoration;
final TextStyle? textStyle;
final Duration? waitDuration;
final Duration? showDuration;
final TooltipTriggerMode? triggerMode;
final bool? enableFeedback;
final VoidCallback? onTriggered;
final Widget? child;

const MyTooltip({
super.key,
this.message,
this.richMessage,
this.height,
this.padding,
this.margin,
this.verticalOffset,
this.preferBelow,
this.excludeFromSemantics,
this.decoration,
this.textStyle,
this.waitDuration,
this.showDuration,
this.triggerMode,
this.enableFeedback,
this.onTriggered,
this.child,
});

@override
Widget build(BuildContext context) {
// 1. CALCULATE FONT SIZE
final fontSize = textStyle?.fontSize == null
? 14 / MediaQuery.textScaleFactorOf(context)
: textStyle!.fontSize! / MediaQuery.textScaleFactorOf(context);
// 2. MERGE INTO textStyle param
final style = textStyle == null
? TextStyle(fontSize: fontSize)
: textStyle!.copyWith(fontSize: fontSize);

return Tooltip(
message: message,
richMessage: richMessage,
height: height,
padding: padding,
margin: margin,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
excludeFromSemantics: excludeFromSemantics,
decoration: decoration,
textStyle: style, // 3. SPECIFY THE SYNTHESIZED PARAM
waitDuration: waitDuration,
showDuration: showDuration,
triggerMode: triggerMode,
enableFeedback: enableFeedback,
onTriggered: onTriggered,
child: child,
);
}
}

A lot of boilerplates, but you can see most part is just passing down the params into Tooltip widget.

The key is MediaQuery.textScaleFactorOf(context) . This gets the current scale factor of text based on device settings. So, what we need is to divide the current font size by this number and erase the effect of the text scale factor.

Controls that specify Text/SelectedText/Tooltip…

In the second tab, I put controls that use widgets to specify texts. I think these consist of the majority of controls in Flutter. Let’s see the implementation.

TextButton(
child: const Text("TextButton widget"),
onPressed: () {
// ignore: avoid_print
print("Button tapped");
},
),
...
ElevatedButton(
child: const Text("ElevatedButton widget"),
onPressed: () {
// ignore: avoid_print
print("Button tapped");
},
),
...
const ListTile(
leading: Icon(Icons.list),
title: Text("ListTile widget"),
tileColor: Colors.grey,
),

For these controls, we can just replace the stock text widgets (Text in this case) with the custom text widgets (MyText) and that would close the case.

TextButton(
child: const MyText("TextButton widget"),
onPressed: () {
// ignore: avoid_print
print("Button tapped");
},
),
...
ElevatedButton(
child: const MyText("ElevatedButton widget"),
onPressed: () {
// ignore: avoid_print
print("Button tapped");
},
),
...
const ListTile(
leading: Icon(Icons.list),
title: MyText("ListTile widget"),
tileColor: Colors.grey,
),

Controls that can specify both string/widget as the text parameter

On the third tab, I listed control(s) that can specify both strings/widgets. (I found only Tab widget as a such example, but if you know more, please let me know 😌).

You might be able to call the answer yourself, but for this case, just make sure you always use the widget parameter over the string parameter and specify your custom widget.

TabBar(
labelColor: Colors.red[400],
unselectedLabelColor: Colors.black87,
tabs: const [
Tab(
text: "Tab defined by a string",
),
Tab(
child: MyText(
"Tab defined by a Text widget",
style: TextStyle(fontSize: 16),
),
),
],
controller: _tabBarController,
),

In this example, the first tab would still show large text due to a string literal, but the second tab would keep the constant size because we specified our custom widget.

Controls with Input

The strings for controls like TextField, and TextFormField comes from the user and not from the app (i.e., you don’t specify the string from inside the app), and that’s why I separated those examples into the fourth tab. However, the approach for these cases is quite similar to the case with Tooltip .

We need to create our custom widget, but since TextField and TextFormField does not have a parameter like textScaleFactor, we need to calculate the font size ourselves and specify that along with textStyle param. Let’s see the implementation of MyTextField .

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class MyTextField extends StatelessWidget {
final TextStyle? style;
final FocusNode? focusNode;
final TextEditingController? controller;
final String? hintText;
final bool? obscureText;
final bool? isMultiline;
final VoidCallback? onEditingComplete;
final Function(String)? onChanged;
final int? minLines;
final int? maxLines;
final Color? cursorColor;
final EdgeInsets? scrollPadding;
final TextInputType? keyboardType;
final bool? enabled;
final InputDecoration? decoration;
final List<TextInputFormatter>? inputFormatters;

const MyTextField({
this.controller,
this.hintText,
this.style,
this.focusNode,
this.scrollPadding,
this.isMultiline,
this.obscureText,
this.onEditingComplete,
this.minLines,
this.maxLines,
this.cursorColor,
this.keyboardType,
this.onChanged,
this.enabled,
this.decoration,
this.inputFormatters,
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
// 1. CALCULATE FONT SIZE
final fontSize = style?.fontSize == null
? 16 / MediaQuery.textScaleFactorOf(context)
: style!.fontSize! / MediaQuery.textScaleFactorOf(context);
// 2. MERGE INTO textStyle param
final textStyle = style == null
? TextStyle(fontSize: fontSize)
: style!.copyWith(fontSize: fontSize);

return TextField(
enabled: enabled,
focusNode: focusNode,
controller: controller,
// 3. SPECIFY THE SYNTHESIZED PARAM
style: textStyle,
onChanged: onChanged,
obscureText: obscureText ?? false,
minLines: minLines,
maxLines: maxLines ?? 1,
onEditingComplete: onEditingComplete,
cursorColor: cursorColor,
keyboardType: keyboardType,
scrollPadding: scrollPadding ?? const EdgeInsets.all(20),
decoration: decoration,
inputFormatters: inputFormatters,
);
}
}

Replace TextField, TextFormField with MyTextField, MyTextFormField and you are done.

Final demo

Let’s see the final product:

As you can see, the text size setting is still large, but the text size in your app stays as in the original (aside from the left top Tab of the third example. Remember that one did not use the custom widget).

Conclusions

  • Flutter complies with the device’s text size settings
  • You can opt out of that by implementing your custom controls and using them everywhere instead of stock controls
  • RichText doesn’t comply with the device’s text size settings
  • However, ideally, you should think of a design/layout that works with device text size so that your app is accessible to people in need
  • Finally, you can see my sample code here: https://github.com/yfujiki/dynamic_size_text

I hope this article helps some developers who are finding layout issues on some customers, but don’t see them on their own devices.

Happy coding!!

--

--

Yuichi Fujiki

Technical director, Freelance developer, a Dad, a Quadriplegic, Life of Rehab