Flutter: Different fonts for different locales

AbdulRahman AlHamali
SAUGO 360
Published in
5 min readAug 22, 2018

“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:

  1. Figure out how to know the currently used locale
  2. 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 this Theme 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 the ThemeData immediately, we provide a textTheme.
  • We assign to this textTheme the textTheme of the parent Theme widget (the default Theme widget that the MaterialApp creates), but we call apply on it to change its fontFamily and its fontSizeDelta, 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!

--

--