Flutter: Different fonts for different locales
“This English font is great for my app, but I need a different font for my Arabic locale”
If you have ever said that when developing a Flutter app, then read along to learn the best way to provide different font families for different locales.
The problem
When writing text to the screen in Flutter, you generally use a Text
widget or some alternative like RichText
with TextSpans
. These widgets have a property known as style
, where you can provide a TextStyle
instance to specify the different stylistic properties of the text, like color, size, and yes, font family:
Text(
'I am a text!',
style: TextStyle(
fontSize: 14.0,
fontFamily: 'ProximaNova'
),
),
Of course, to use a font family, it needs to either be a system font, or a custom font imported as explained in the tutorial
.
However, in a real-world scenario, you wouldn’t want to set the font separately for each and every widget. Instead, you would want a global default font that you can override in specific scenarios. To do that, Flutter allows us to set the fontFamily
in the ThemeData
constructor, which we can then feed into our MaterialApp
’s theme
, to apply it app-wide:
MaterialApp(
theme: ThemeData(
fontFamily: 'ProximaNova'
),
home: HomeScreen()
);
However, what can we do to change this font depending on our current locale? There are two things that we need to do:
- Figure out how to know the currently used locale
- Use that locale in the right place to determine the font
How to know the currently used locale
This part is easy, we can use the Localizations
widget to inquire about the locale used in a specific context:
var locale = Localizations.localeOf(context)
shameless plug: I’ve written another article about the different aspects of managing locale in Flutter, you might be interested in it.
Using the locale to determine the font
It looks easy now, you might think, we just need to check the locale, and provide the corresponding font family:
var lang = Localizations.localeOf(context).languageCode;
return MaterialApp(
theme: ThemeData(
fontFamily: lang == 'ar'? 'FrutigerLTArabic' : 'ProximaNova'
),
home: HomeScreen()
);
Unfortunately, you will be faced with the exception:
a Localizations ancestor was not found
Why does this exception happen?
This kind of exception is a common occurence in Flutter, it simply means that you’re using the wrong BuildContext
. When we call Localizations.localeOf(context)
, what flutter does under the hood is to take this context
, and go up in the widget tree, looking for some ancestor whose type is Localizations
. It will not find any, though, because the MaterialApp
itself is the one that creates this Localizations
widget, and the context
that we used is outside the MaterialApp
. So, you might think that a small change to the code could fix this:
return MaterialApp(
theme: ThemeData(
fontFamily:
Localizations.localeOf(context).languageCode == 'ar'?
'FrutigerLTArabic' : 'ProximaNova'
),
home: HomeScreen()
);
Our context
is now inside the MaterialApp
, right? Nope! This is what your app probably looks like:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
fontFamily:
Localizations.localeOf(context).languageCode == 'ar'?
'FrutigerLTArabic' : 'ProximaNova'
),
home: HomeScreen()
);
}
}
So, the context
that you’re using is the same context
provided to the build
function of MyApp
, which is still outside the MaterialApp
. Where, then, can we get a context that is inside the MaterialApp
, but still outside all of our widgets?
The builder callback of the MaterialApp
The MaterialApp
provides the builder
callback just for those scenarios. The documentation defines this property:
A builder for inserting widgets above the Navigator but below the other widgets created by the MaterialApp widget, or for replacing the Navigator entirely.
Which is great for our purposes. Here, we are outside of all our widgets, but we still have access to the widgets that the MaterialApp
creates, like Localizations
, Theme
, MediaQuery
, etc.
The builder
provides two parameters, the context
, which we can use to access the Localizations
or other widgets, and the main Navigator
instance of our app, which is provided to us for a reason that we will know shortly.
Using the builder to solve our problem
We can now use the builder
callback as follows:
return MaterialApp(
home: HomeScreen(),
builder: (context, navigator) {
var lang = Localizations.localeOf(context).languageCode;
return Theme(
data: ThemeData(
fontFamily: lang == 'ar'? 'FrutigerLTArabic' : 'ProximaNova'
),
child: navigator,
);
},
);
- We returned a
Theme
widget, and provided this theme with the font family depending on the locale. - We provided the
navigator
instance as a child to thisTheme
widget.
What we did here was like injecting a Theme
widget in the middle between the navigator
and its parent. The parent’s child was supposed to be the navigator
. However, we put the Theme
widget instead, and provided the navigator
as its child to continue the normal widget tree.
And that’s it! If you try it you will see that we have successfully used the builder
property of the MaterialApp
, to provide different fonts for different locales!
Bonus Tip: different font sizes for different locales
Before we leave, let’s also consider the fact that in real-world use cases, we might also want to change the font size depending on the locale. Font sizes don’t always match: a 14.0 in ProximaNova is much smaller than a 14.0 in FrutigerLTArabic. To account for this, we can use TextTheme.apply
method. This method takes a given TextTheme
, and applies it to another theme with a few changes. Let’s take a look at this code:
return MaterialApp(
home: HomeScreen(),
builder: (context, navigator) {
var lang = Localizations.localeOf(context).languageCode;
return Theme(
data: ThemeData(
textTheme: Theme.of(context).textTheme.apply(
fontFamily: lang == 'ar'?
'FrutigerLTArabic' : 'ProximaNova',
fontSizeDelta: lang == 'ar'? -5.0 : 0.0
),
),
child: navigator,
);
},
);
- We are doing the same as we did earlier, but instead of providing a
fontFamily
to theThemeData
immediately, we provide atextTheme
. - We assign to this
textTheme
thetextTheme
of the parentTheme
widget (the defaultTheme
widget that theMaterialApp
creates), but we callapply
on it to change itsfontFamily
and itsfontSizeDelta
, depending on our locale.
The fontSizeDelta
is the key here. This property allows us to specify how much difference we want between the specified size and the effective size of the font. This means that if we have a Text
widget that looks like:
Text(
'I am a text!',
style: TextStyle(
fontSize: 14.0,
),
);
and we set the fontSizeDelta
to -5.0. Then the effective font size of the Text
will be 9.0.
And by that, we can mitigate the difference in the size between the different fonts. Note though that you can specify the difference in the font size in another format, using fontSizeFactor
instead of fontSizeDelta
. Which multiplies the specified font size by the fontSizeFactor
. So if, for example, we had a font size of 14.0, and a fontSizeFactor
of 0.5, then the effective font size will be 7.0.
In general, we can use fontSizeFactor
and fontSizeDelta
together, which affect any given font size like:
fontSize = specifiedFontSize * fontSizeFactor + fontSizeDelta
Allowing us to flexibly change the font size as needed.
I hope this has been useful!
This is now my second article tackling the different aspects of managing locale in Flutter. And as I learn more, I will definitely share more! Stay tuned!