Mastering TextStyle - Part 1
How Flutter's Widget Tree Provides the Style to Text
Let's say placing Text
with its ancestor MaterialApp
and Scaffold
, you will see the black-colored text on your screen.
void main() {
runApp(
const MaterialApp(
home: Scaffold(
body: Text('Hi, it\'s Chooyan Here'),
),
),
)
}
Now, can you imagine what will happen if we omit Scaffold
or MaterialApp
from the code above? Let me test those cases.
When I first remove Scaffold
, but MaterialApp
is still there, red-colored yellow-underlined text appeared.
If I remove MaterialApp
but Scaffold
is there, on the other hand, black-colored text appeared again.
Curiously, the appearance of Text
changes even though we don't give any TextStyle
through style
argument. It changes depending on what widgets are on its ancestor in reality.
That is the power of Flutter's widget tree, and we will discuss how Text
finds TextField
from the widget tree in this article, and also discuss how we can maximize the potential of this mechanism in our app in the next article.
Text and Its Implementation
Let's get started by taking a closer look at Text
.
Text
is a subclass of StatelessWidget
, and it composes what to display on users' screens in the build()
method as we do in our StatelessWidget
s.
Thus, if we want to know how Text
decides what style it applies in reality, we can jump into the build()
method and read the implementation.
Once we head to text.dart
in the Flutter framework, we soon find build()
method starting with the code below.
class Text extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
TextStyle? effectiveTextStyle = style;
if (style == null || style!.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(style);
}
...
}
}
As we can see, Text
tries to find DefaultTextStyle
by calling DefaultTextStyle.of(context)
.
DefaultTextStyle.of()
is a method that finds DefaultTextWidget
from context
's ancestor on the widget tree. If no DefaultTextStyle
is found, empty TextStyle
will be returned as DefaultTextStyle.fallback()
.
class DefaultTextStyle extends InheritedTheme {
static DefaultTextStyle of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<DefaultTextStyle>() ?? const DefaultTextStyle.fallback();
}
}
In other words, if we want to find out how Text
decides its style without style
argument given, what we need to do is find out widgets that place DefaultTextStyle
in their build()
method.
The Strategy of MaterialApp and Scaffold
So, let's step into the discussion of how MaterialApp
and Scaffold
places DefaultTextStyle
in their build()
method.
MaterialApp
Because MaterialApp
is a subclass of StatefulWidget
, it's build()
method is defined in its associated State
class, named _MaterialAppState
.
Reading the build()
method and related internal methods, the code below can be found.
class _MaterialAppState extends State<MaterialApp> {
...
Widget _buildWidgetApp(BuildContext context) {
return WidgetsApp.router(
...
textStyle: _errorTextStyle,
);
}
}
const TextStyle _errorTextStyle = TextStyle(
color: Color(0xD0FF0000),
fontFamily: 'monospace',
fontSize: 48.0,
fontWeight: FontWeight.w900,
decoration: TextDecoration.underline,
decorationColor: Color(0xFFFFFF00),
decorationStyle: TextDecorationStyle.double,
debugLabel: 'fallback style; consider putting your text in a Material',
);
The _errorTextStyle
is the exact one that is referred to display the red-colored yellow-underlined text appearing when Scaffold
is omitted.
The textStyle
passed to WidgetsApp
is used to build DefaultTextStyle
in _WidgetsAppState
with the code below.
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
...
@override
Widget build(BuildContext context) {
if (widget.textStyle != null) {
result = DefaultTextStyle(
style: widget.textStyle!,
child: result,
);
}
}
}
Briefly summarising the widget tree constructed by MaterialApp
, we can illustrate the situation below.
Scaffold
Next, we will focus on Scaffold
and how it provides DefaultTextStyle
to its descendant Text
s.
Scaffold
is a subclass of StatefulWidget
, so we can jump to ScaffoldState
and read build()
method. However, when we try to find DefaultTextStyle
in the scaffold.dart
file, we soon find that zero result is hit, in other words, Scaffold
or ScaffoldState
don't directly place DefaultTextStyle
in their build()
method. So who does?
Instead of DefaultTextStyle
, ScaffoldState
places Material
on the widget tree.
class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, RestorationMixin {
...
@override
Widget build(BuildContext context) {
...
child: Material(
color: widget.backgroundColor ?? themeData.scaffoldBackgroundColor,
...
),
}
}
Material
is also a StatefulWidget
and _MaterialState
has an implementation that we now want to find out.
class _MaterialState extends State<Material> with TickerProviderStateMixin {
...
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
...
Widget? contents = widget.child;
if (contents != null) {
contents = AnimatedDefaultTextStyle(
style: widget.textStyle ?? Theme.of(context).textTheme.bodyMedium!,
duration: widget.animationDuration,
child: contents,
);
}
}
}
_MaterialState
first find ThemeData
from the widget tree by calling Theme.of(context)
, and then create AnimatedDefaultTextStyle
, which is an "Animated" version of DefaultTextStyle
, and passing Theme.of(context).textTheme.bodyMedium
to style
argument (widget.textStyle
is null
in this case).
Looking back on the usage of MaterialApp
, ThemeData
is an object that we can pass to MaterialApp
through theme
argument.
In short, Scaffold
internally places DefaultTextStyle
using textTheme.bodyMedium
exposed by ThemeData
provided by MaterialApp
on the widget tree.
Check the situation so far with the image below.
We can see that error-styled DefaultTextStyle
is still on the widget tree but is not referred to by Text
s below Scaffold
anymore because Scaffold
provides another DefaultTextStyle
. That is why we never see error-styled texts in our app as long as we use Scaffold
at every page.
Conclusion
In this article so far, we have discussed how Text
finds DefaultTextStyle
from the widget tree, and how MaterialApp
and Scaffold
place DefaultTextStyle
on the widget tree with reading internal source codes.
What is interesting here is that DefaultTextStyle
can be overridden by a descendant. Text
never use MaterialApp
's DefaultTextStyle
, which is an error-styled one, but uses its descendant Scaffold
's because DefaultTextStyle.of(context)
returns the closest ancestor DefaultTextStyle
from the widget tree.
In other words, if we want to make it clear what style would be applied to each Text
, what we should do is to explorer DefaultTextStyle
provided by the closest ancestor.
This mechanism is nicely used in various kinds of widgets, such as AppBar
, Dialog
, etc, but we also encounter troubles related to DefaultTextStyle
, when using Overlay
for example.
We will discuss them in detail in the next article. Thank you for reading.