Mastering styled text in Flutter
You will need a Flutter development environment set up on your machine.
Introduction
In this tutorial we are going to start with an overview of Dart strings and Unicode. Next we’ll move on to styling text for your app, first for entire strings and then for spans within a string.
Prerequisites
To go through this tutorial you should have the Flutter development environment set up and know how to run an app. I’m using Android Studio with the Flutter 1.1 plugin, which uses Dart 2.1.
Setup
Create a new Flutter app. I’m calling mine flutter_text
.
Open main.dart
and replace the code with the following:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text('Styling text')),
body: Container(
child: Center(
child: _myWidget(context),
),
),
),
);
}
}
// modify this widget with the example code below
Widget _myWidget(BuildContext context) {
String myString = 'I ❤️ Flutter';
print(myString);
return Text(
myString,
style: TextStyle(fontSize: 30.0),
);
}
Note the _myWidget()
function at the end. You can modify or replace it using the examples below. The more you experiment on your own, the more you will learn.
If you are already familiar with concepts like grapheme clusters and Dart strings, you can skip down to the text styling sections below.
Unicode
Coded messages
When I was a kid I liked to write “secret” messages in code, where 1=a, 2=b, 3=c and so on until 26=z. A message using this code might be:
9 12 9 11 5 6 12 21 20 20 5 18
To make the code even more secret you could shift the numbers, where 1=b, 2=c, 3=d and so on until it wrapped around where 26=a. As long as my friend and I had the same code key, we could decode each other’s messages. The wrong code key, though, would give garbled nonsense.
Computers are similar, except most of the time we don’t want secret messages. We want to make our messages easy to decode, so we agree on a code key, or should I say, a standard. ASCII was an early example of this, where the code key was 97=a, 98=b, 99=c, and so on. That worked fine for English but ASCII only had 128 codes (from 7 bits of data) and that wasn’t enough for all of the characters in other languages. So people made other code keys with more numbers. The problem was that the numbers overlapped and when you used the wrong decoding key you ended up with garbled nonsense.
Unicode to the rescue
Unicode is an international standard that assigns unique code numbers for the characters of every language in the world. The code numbers are called code points. In addition to what we normally think of as characters, there are also code points for control characters (like a new line), diacritical marks (like the accent over an é), and pictures (like 😊). As long as everyone agrees to use this code standard, there are no more fewer garbled messages.
Unicode is just a long list of code points. Saving these code points or sending them is another matter. To help you understand this, take my secret message from above as an example. If I write it as a string of numbers without whitespace and try to send it to you, you get:
9129115612212020518
This is almost impossible to decode now. Does 912
mean 9
, 1
, 2
or does it mean 9
, 12
? It’s the same situation with Unicode. We have to use an agreed upon means to save and send Unicode text, or else it would be very difficult to decode. There are three main ways to do it: UTF-8, UTF-16, and UTF-32. UTF stands for Unicode Transformation Format, and each method of encoding has its advantages and disadvantages.
- UTF-8 saves each code point using one to four bytes of data.
- UTF-16 saves each code point as two or four bytes of data. One 16-bit code unit is big enough to uniquely reference a lot of Unicode code points, but not big enough for all of them (emojis, for example). In order to save code points with numbers higher than 16 bits (that is, higher than the number 65,535), UTF-16 uses two 16-bit code units (called surrogate pairs) to map the other code points.
- UTF-32 saves each code point using four bytes of data. It provides a direct one-to-one mapping of UTF-32 code units to Unicode code points.
When working with UTF-16 code units, you need to be careful not to forget about the other half of a surrogate pair. And even if you are working with UTF-32, you shouldn’t assume that a single code point is the same as what a user perceives to be a character. For example, country flags (like 🇨🇦) are made of two code points. An accented character (like é) can also optionally be made from two code points. In addition to this, there are emoji with skin tone (like 👩🏾, 2 code points) and family emoji (like 👨👩👧, 5 code points).
So as a programmer, it is better not to think of UTF code units or Unicode code points as characters themselves. That will lead to bugs (for example, when trying to move the cursor one place to the left). Instead, you should think about what Unicode calls a grapheme cluster. These are user-perceived characters. So 🇨🇦, é, 👩🏾, and 👨👩👧 are each a single grapheme cluster because they each look like a single character even though they are made up of multiple Unicode code points.
Further reading
If you find this interesting or would like a deeper understand of the issues related to Unicode, I encourage you to read the following articles:
- Let’s Stop Ascribing Meaning to Code Points
- Dark corners of Unicode
- Strings in Swift 4 (Although this article is about Swift, it has an excellent discussion of the issues involved.)
Dart strings
Let’s move on from talking about Unicode in a general way to seeing how Dart uses it.
Code units
In Dart, strings are sequences of UTF-16 code units. That makes string manipulation look deceptively easy because you can get the string value of a code unit by a random integer index:
String myString = 'Flutter';
String myChar = myString[0]; // F
But this creates bugs if you split a surrogate pair.
String myString = '🍎'; // apple emoji
List<int> codeUnits = myString.codeUnits; // [55356, 57166]
String myChar = myString[0]; // 55356 (half of a surrogate pair)
This will throw an exception if you try to display myChar
in a Text widget.
Runes
A better alternative is to work with code points, which are called runes in Dart.
String myString = '🍎π';
List<int> codeUnits = myString.codeUnits; // [55356, 57166, 960]
int numberOfCodeUnits = myString.length; // 3
int firstCodeUnit = myString.codeUnitAt(0); // 55356
Runes runes = myString.runes; // (127822, 960)
int numberOfCodPoints = runes.length; // 2
int firstCodePoint = runes.first; // 127822
Grapheme clusters
Even runes will fail when you have grapheme clusters composed of multiple code points.
String myString = '🇨🇦';
Runes runes = myString.runes; // (127464, 127462)
int numberOfCodePoints = runes.length; // 2
int firstCodePoint = runes.first; // 127464
String halfFlag = String.fromCharCode(firstCodePoint); // 🇨
Displaying the halfFlag
string in your app won’t crash it, but users will perceive it as a bug since it only contains one of the two regional indicator symbols used to make the Canadian flag.
Unfortunately, at the time of this writing, there is no support for grapheme clusters in Dart, though there is talk of implementing it. You should still keep them in mind while writing tests and working with strings, though.
Hexadecimal notation
If you are starting with a Unicode hex value, this is how you get a string:
String s1 = '\u0043'; // C
String s2 = '\u{43}'; // C
String s3 = '\u{1F431}'; // 🐱 (cat emoji)
String s4 = '\u{65}\u{301}\u{20DD}'; // é⃝ = "e" + accent mark + circle
int charCode = 0x1F431; // 🐱 (cat emoji)
String s5 = String.fromCharCode(charCode);
Substrings
The String documentation (here and here) is pretty good, and you should read it if you haven’t already. I want to review substrings before we go on to text styling, though, since we will be using it later.
To get a substring you do the following:
String myString = 'I ❤️ Flutter.';
int startIndex = 5;
int endIndex = 12;
String mySubstring = myString.substring(startIndex, endIndex); // Flutter
You can find index numbers with indexOf()
:
int startIndex = myString.indexOf('Flutter');
OK, that’s enough background information. Let’s get on to styling text in Flutter.
Text styling with the Text widget
We are going to look first at styling strings in a Text widget. After that we will see how to style substrings within a RichText widget. Both of these widgets use a TextStyle widget to hold the styling information.
Replace _myWidget()
with the following code:
Widget _myWidget(BuildContext context) {
return Text(
'Styling text in Flutter',
style: TextStyle(
fontSize: 30.0,
),
);
}
Or, if you would like to compare multiple style settings at once, you can use the following column layout.
Widget _myWidget(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Styling text in Flutter',
style: TextStyle(
fontSize: 8,
),
),
Text(
'Styling text in Flutter',
style: TextStyle(
fontSize: 12,
),
),
Text(
'Styling text in Flutter',
style: TextStyle(
fontSize: 16,
),
),
],
);
}
Note that I am setting the TextStyle using the style
property of the Text widget. I will modify the TextStyle options below. Try them out yourself by pressing hot reload between every change. You may want to leave a large font size (like fontSize: 30
) for some of the later examples below so that you can see what is happening.
Text size
TextStyle(
fontSize: 30.0,
)
When fontSize
is not given, the default size is 14 logical pixels. Logical pixels are independent of a device’s density. That is, the text should appear to be to be basically the same size no matter what the pixel density of a user’s device may be. However, this font size is also multiplied by a textScaleFactor
depending on the user’s preferred font size.
If you wish to disable accessibility scaling, you can set it on the Text widget. (I’m very impressed that Flutter has accessibility enabled by default, and I definitely don’t suggest that you disable it without reason. In some rare cases, though, an oversized font might break a layout…in which case it would still probably be better to redesign your layout rather than disable accessibility.)
// This text will always display at 30.0 logical pixels, no matter
// what the user's preferred size is.
Text(
'Some text',
textScaleFactor: 1.0, // disables accessibility
style: TextStyle(
fontSize: 30.0
),
)
You can also use the theme data to set the text size. See the section on themes below.
Text color
TextStyle(
color: Colors.green,
)
In addition to predefined colors like Colors.green
and Colors.red
, you can also set shades on a color, like Colors.blue[100]
or Colors.blue[700]
.
Background color
Widget _myWidget(BuildContext context) {
Paint paint = Paint();
paint.color = Colors.green;
return Text(
'Styling text in Flutter',
style: TextStyle(
background: paint,
fontSize: 30.0,
),
);
}
For a Text widget you could also just wrap it in a Container and set the color on the Container.
Bold
TextStyle(
fontWeight: FontWeight.bold,
)
You can set the weight with numbers like FontWeight.w100
where w400
is the same as normal
and w700
is the same as bold
.
Italic
TextStyle(
fontStyle: FontStyle.italic,
)
The only choices are italic
and normal
.
Shadow
TextStyle(
shadows: [
Shadow(
blurRadius: 10.0,
color: Colors.blue,
offset: Offset(5.0, 5.0),
),
],
)
When setting the shadow you can change the blur radius (bigger means more blurry), color, and offset. You can even set multiple shadows as if there were more than one light source.
TextStyle(
shadows: [
Shadow(
color: Colors.blue,
blurRadius: 10.0,
offset: Offset(5.0, 5.0),
),
Shadow(
color: Colors.red,
blurRadius: 10.0,
offset: Offset(-5.0, 5.0),
),
],
)
I’m not sure if more than one shadow is useful or not, but it is interesting.
Underline
TextStyle(
decoration: TextDecoration.underline,
decorationColor: Colors.black,
decorationStyle: TextDecorationStyle.solid,
)
The decoration can be underline
, lineThrough
, or overline
. The last line of text in the image above has an overline.
The choices for decorationStyle are solid
, double
, dashed
, dotted
, and wavy
.
Spacing
TextStyle(
letterSpacing: -1.0,
wordSpacing: 5.0,
)
In the example image, the six lines on top use letter spacing ranging from -2.0 to 3.0. The six lines on bottom use word spacing ranging from -3.0 to 12.0. A negative value moves the letters or words closer together.
Font
Using a custom font requires a few more steps:
- Add a directory called
assets
to the root of your project. - Copy a font into it. (I downloaded the Dancing Script font from here, unzipped it, and renamed the regular one to
dancing_script.ttf
.) - In
pubspec.yaml
register the font:
flutter:
fonts:
- family: DancingScript
fonts:
- asset: assets/dancing_script.ttf
- Set the font in your widget:
TextStyle(
fontFamily: 'DancingScript',
)
- Do a full stop and restart.
See this post for more help.
Using themes
Our root widget is a MaterialApp widget, which uses the Material Design theme. Through the BuildContext we have access to its predefined text styles. Instead of creating our own style with TextStyle, you can use a default one like this:
Text(
'Styling text in Flutter',
style: Theme.of(context).textTheme.title,
)
That was the default style for titles. There are many more defaults for other types of text. Check them out:
If a style is not specified, Text uses the DefaultTextStyle. You can use it yourself like this:
Text(
'default',
style: DefaultTextStyle.of(context).style,
)
DefaultTextStyle gets its style from the build context.
See the documentation for more about using themes.
Text styling with the RichText widget
The final thing I want to teach you is how to style part of a text string. With a Text widget the whole string has the same style. A RichText widget, though, allows us to add TextSpans that include different styles.
Basic example
Replace _myWidget()
with the following code:
Widget _myWidget(BuildContext context) {
return RichText(
text: TextSpan(
// set the default style for the children TextSpans
style: Theme.of(context).textTheme.body1.copyWith(fontSize: 30),
children: [
TextSpan(
text: 'Styling ',
),
TextSpan(
text: 'text',
style: TextStyle(
color: Colors.blue
)
),
TextSpan(
text: ' in Flutter',
),
]
)
);
}
Note: An alternate way to make text with styled spans is to use the
Text.rich()
constructor, which has the same default style as the Text widget.
RichText takes a TextSpan tree. Every very TextSpan takes more TextSpan children, which inherit the style of their parent. To make the word “text” blue, I had to divide the string into three TextSpans. I used a color for the style, but I could have just as easily used any of the other styles that we have already looked at. Try adding a few more styles yourself.
Styling programmatically
In a real application we would probably have a longer string. For example, let’s highlight every occurrence of “text” in the following string:
To do that we have to look at the string and find the indexes of the text that we want to style. Then we use substring to cut the string up and put it in a list of TextSpans.
Replace _myWidget()
with the following code:
Widget _myWidget(BuildContext context) {
final String myString =
'Styling text in Flutter Styling text in Flutter '
'Styling text in Flutter Styling text in Flutter '
'Styling text in Flutter Styling text in Flutter '
'Styling text in Flutter Styling text in Flutter '
'Styling text in Flutter Styling text in Flutter ';
final wordToStyle = 'text';
final style = TextStyle(color: Colors.blue);
final spans = _getSpans(myString, wordToStyle, style);
return RichText(
text: TextSpan(
style: Theme.of(context).textTheme.body1.copyWith(fontSize: 30),
children: spans,
),
);
}
List<TextSpan> _getSpans(String text, String matchWord, TextStyle style) {
List<TextSpan> spans = [];
int spanBoundary = 0;
do {
// look for the next match
final startIndex = text.indexOf(matchWord, spanBoundary);
// if no more matches then add the rest of the string without style
if (startIndex == -1) {
spans.add(TextSpan(text: text.substring(spanBoundary)));
return spans;
}
// add any unstyled text before the next match
if (startIndex > spanBoundary) {
spans.add(TextSpan(text: text.substring(spanBoundary, startIndex)));
}
// style the matched text
final endIndex = startIndex + matchWord.length;
final spanText = text.substring(startIndex, endIndex);
spans.add(TextSpan(text: spanText, style: style));
// mark the boundary to start the next search from
spanBoundary = endIndex;
// continue until there are no more matches
} while (spanBoundary < text.length);
return spans;
}
Experiment with changing the search word and style.
In this example we searched for plain text, but you can also do pattern matching using regular expressions.
Clickable spans
You can make a span clickable by adding a TapGestureRecognizer:
TextSpan(
text: spanText,
style: style,
recognizer: TapGestureRecognizer()
..onTap = () {
// do something
},
)
This would allow you to open a URL, for example, if used along with the url_launcher plugin.
Final notes
Here are a few more related concepts that I didn’t have time or space to cover:
- If you have HTML text, you can format it by using a plugin. None of the plugins I tested render everything perfectly, but the best one seemed to be flutter_html_view.
- If you need to style text that you are painting on a canvas, check out the TextPainter class.
- For converting text into UTF-8, you can import
dart:convert
.
Conclusion
Text seems like it should be so simple, but it really isn’t. Language is messy and dealing with it as a programmer can be difficult. Much progress has been made in recent years, though. Unicode has solved a lot of problems. Dart and Flutter also give us a lot of tools to manipulate and style text. I expect to see these tools improve even more in the future.
The source code for this project is available on GitHub.
By the way, in case you were curious but lazy, my secret message was “I like Flutter”.
28 February 2019
by Suragch