Managing Locale in Flutter
The internalization tutorial on Flutter’s website teaches us how to set up our app in a way to support multiple locales. Something along those lines:
import 'package:flutter_localizations/flutter_localizations.dart';
MaterialApp(
localizationsDelegates: [
// ... app-specific localization delegate[s] here
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // English
const Locale('ar', ''), // Arabic
// ... other locales the app supports
],
// ...
)
The tutorial then explains in detail how to set up your localized values for each locale that your app supports.
But how do you choose which locale to show your user? The tutorial above mentions that the application-wide device locale will be used by default. But what to do if we want something else? We might, for example, want to allow our user to choose the locale from the app, or we might want to fetch the locale from a remote server. Whatever the reason, it is important to be able to easily set the locale to something different than that of the device, and Flutter provides us with just the right tools for the job.
Note: Our tutorial assumes that you have a basic understanding of internalization in Flutter. If you don’t, please make sure to review the official internationalization tutorial before continuing.
Let’s get started with a simple localized app:
void main() => runApp(MyApp());
class MyApp extends StatlessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
MyLocalizationsDelegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('ar', ''), // Arabic
],
home: HomeScreen()
);
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
TextStyle selectedStyle = TextStyle(
color: Theme.of(context).accentColor
);
return Scaffold(
appBar: AppBar(),
body: Center(child: Text(MyLocalizations.of(context, 'foo'))),
);
}
}
- The code above defines a simple app that supports both the English and Arabic Languages
- The app defines its own localizations in a class called MyLocalizations (we will see it in a bit)
- The app displays the localized word ‘foo’ in the middle of the screen. The word is displayed either in English or in Arabic depending on the chosen locale.
MyLocalizations simply looks like this:
class MyLocalizations {
MyLocalizations(this.locale);
final Locale locale;
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'foo': 'Foo',
'bar': 'Bar'
},
'ar': {
'foo': 'فو',
'bar': 'بار'
}
};
String translate(key) {
return _localizedValues[locale.languageCode][key];
}
static String of(BuildContext context, String key) {
return Localizations.of<MyLocalizations>(context,
MyLocalizations).translate(key);
}
}
class MyLocalizationsDelegate extends
LocalizationsDelegate<MyLocalizations> {
const MyLocalizationsDelegate();
@override
bool isSupported(Locale locale) =>
['en', 'ar'].contains(locale.languageCode);
@override
Future<MyLocalizations> load(Locale locale) {
return SynchronousFuture<MyLocalizations>
(MyLocalizations(locale));
}
@override
bool shouldReload(MyLocalizationsDelegate old) => false;
}
- This is almost a textbook localizations class like the one you learned in the internationalization tutorial
- I just made my
of
function take a key and return the localized string immediately.
Now, let’s get started! We want to control which locale is fed into this app!
The locale property of the MaterialApp
The first, and easiest way, which weirdly enough is not mentioned in the internationalization tutorial, is using the locale property. This property of the MaterialApp
class allows us to immediately specify what locale we want our app to use. So we can write:
return MaterialApp(
locale: Locale('ar', ''),
localizationsDelegates: [
MyLocalizationsDelegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('ar', ''), // Arabic
],
home: HomeScreen()
);
This locale
property that we set here allows us to force the locale of the app to Arabic, regardless of the locale of the device.
Fetching the locale from a server (or anywhere basically)
Of course, in most cases we wouldn’t want a hard-coded value. We would want to fetch the locale from a server, from the SharedPreferences
, or from some file. Let’s assume we have the following function which fetches the locale info from the shared preferences:
_fetchLocale() async {
var prefs = await SharedPreferences.getInstance();
return Locale(prefs.getString('language_code'),
prefs.getString('country_code'));
}
We want to call this function, and then use the value it returns as a locale for our MaterialApp
. To do that, we can either use a FutureBuilder
, or make MyApp
stateful, and call _fetchLocale
in theinitState
function. Let’s check out both approaches!
The first is to use FutureBuilder
as follows:
return FutureBuilder(
future: this._fetchLocale(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
// Return some loading widget
return CircularProgressIndicator();
case ConnectionState.done:
if (snapshot.hasError) {
// Return some error widget
return Text('Error: ${snapshot.error}');
} else {
Locale fetchedLocale = snapshot.data;
return MaterialApp(
locale: fetchedLocale,
localizationsDelegates: [
MyLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // English
const Locale('ar', ''), // Arabic
],
home: HomeScreen()
);
}
}
},
);
- This
FutureBuilder
, is calling the_fetchLocale
function, and when it returns its value, theFutreBuilder
feeds that value into ourMaterialApp
throughsnapshot.data
. - This approach is elegant, it does not require defining variables to detect loading, error, etc.
The other approach uses the initState
function:
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Locale locale;
@override
void initState() {
super.initState();
this._fetchLocale().then((locale) {
setState(() {
this.locale = locale;
});
});
}
@override
Widget build(BuildContext context) {
if (this.locale == null) {
return CircularProgressIndicator();
} else {
return MaterialApp(
locale: this.locale,
localizationsDelegates: [
MyLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // English
const Locale('ar', ''), // Arabic
],
home: HomeScreen()
);
}
}
_fetchLocale() async {
var prefs = await SharedPreferences.getInstance();
return Locale(prefs.getString('language_code'),
prefs.getString('country_code'));
}}
- Notice that we are keeping an instance of the locale inside the class
- In
initState
, we are calling_fetchLocale
and storing its result in the locale instance. We callsetState
to make sure the app rebuilds when we receive the locale. - In the build function, as long as the locale is still null, we show a progress indicator. Otherwise, we show the
MaterialApp
with the locale instance fed into it. - Using
FutureBuilder
might be cleaner and more preferred in most cases, but we will see why it might not be the best idea, when we get to talking about changing the locale upon the user’s request.
Showing the user the selected locale
Now that our app effectively fetches and uses the desired locale. Let’s see how we can display to the user the available locales, while highlighting the one in use. Let’s show our locales in the side drawer, and display the selected locale with the accent color, while the other locale is displayed with the default color.
To find out the chosen locale from anywhere in our application, we can use Localizations.localeOf(context)
. This function provides us with the selected locale for any build context (Typically, the locale will be the same for the whole application, but it is possible to define a different locale for only part of the app). So, we will add this side drawer to our HomeScreen widget, and show the active locale distinctly:
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
TextStyle selectedStyle = TextStyle(
color: Theme.of(context).accentColor
);
return Scaffold(
appBar: AppBar(),
drawer: Drawer(
child: ListView(
children: <Widget>[
ListTile(
title: Text(
'العربية', // which means Arabic
style: _getLanguageCode(context) == 'ar'?
selectedStyle : null,
)
),
ListTile(
title: Text(
'English',
style: _getLanguageCode(context) == 'en'?
selectedStyle : null,
),
),
],
),
),
body: Center(child: Text('Managing your locale')),
);
}
_getLanguageCode(BuildContext context) {
return Localizations.localeOf(context).languageCode;
}
}
- We have added a drawer which displays the available locales, and highlights the color of the selected locale using the the
_getLanguageCode
function. - The
_getLanguageCode
function simply returns the language code returned byLocalizations.localeOf(context)
.
Allowing the user to change the locale
The final step is to allow the user to change the displayed locale. When the user clicks on the language name in the drawer, we want to rebuild the app using the chosen locale. To do this, we will implement the onTap event for the two entries in the drawer. When an entry is tapped, we do two things:
- store the new locale in the shared preferences or on the server or wherever we choose, so that when the application is restarted, the new locale is shown.
- Rebuild the whole
MaterialApp
to show it with the new locale
So, the onTap event for the English entry, for example, will look like:
onTap: () async {
// If the already-selected language is not English
// Then, change it to English
if (_getLanguageCode(context) != 'en') {
// step one, save the chosen locale
var prefs = await SharedPreferences.getInstance();
await prefs.setString('language_code', 'en');
await prefs.setString('country_code', '');
// step two, rebuild the whole app, with the new locale
}
}
But how will we rebuild the whole app? To do that, we need to call setState
in _MyAppState
, and provide it with the new locale. One way to achieve that is to create an event that bubbles all the way from the tapped entry, to _MyAppState
widget, where we call setState
to change the locale. But this is too much of a hassle, because what if the tapped entry where we are changing the locale is buried too deep in the widget tree, we will then have to add code everywhere just to bubble the event up to the main app widget. We can definitely do better than this!
Let’s add the following function inside MyApp
widget:
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
static void setLocale(BuildContext context, Locale newLocale) {
_MyAppState state =
context.ancestorStateOfType(TypeMatcher<_MyAppState>());
state.setState(() {
state.locale = newLocale;
});
}
}
- The
setLocale
static function can be called from any context. It uses theancestorStateOfType
method to locate the_MyAppState
object. - Once the objectis located, the function simply calls
setState
on it, and provides it with the new locale.
We can use this function then from the onTap event:
onTap: () async {
// If the chosen language is not English
// Then, let's change it to English
if (_getLanguageCode(context) != 'en') {
// step one, save the chosen locale
var prefs = await SharedPreferences.getInstance();
await prefs.setString('languageCode', 'en');
await prefs.setString('countryCode', '');
// step two, rebuild the whole app, with the new locale
MyApp.setLocale(context, Locale('en', ''));
}
}
This will rebuild our MaterialApp
with the new locale.
You can notice here that this is the reason why I preferred not to use FutureBuilder
, and to handle the future in initState
instead. Using FutureBuilder
would have meant that the future will be called every time the app is rebuilt, which is not what we want. Thus we used initState
instead, which, by definition, will only be called once.
Taking the device locale into account
We have spent this whole article running away from the device locale. But let’s face it, we might still need it. What if nothing is stored in the shared preferences yet? What if our server did not reply? can’t we fall back to using the device locale then?
Of course we can! All we have to do is to replace the locale
property that we learned in the beginning of the tutorial, with the more powerful localeResolutionCallback
property, which is a callback that accepts the device locale as a parameter, and let’s you return whatever locale you want. Thus, let’s play a little bit with _MyAppState
class to make it look as follows:
class _MyAppState extends State<MyApp> {
Locale locale;
bool localeLoaded = false;
@override
void initState() {
super.initState();
this._fetchLocale().then((locale) {
setState(() {
this.localeLoaded = true;
this.locale = locale;
});
});
}
@override
Widget build(BuildContext context) {
if (this.localeLoaded == false) {
return CircularProgressIndicator();
} else {
return MaterialApp(
localeResolutionCallback: (deviceLocale, supportedLocales) {
if (this.locale == null) {
this.locale = deviceLocale;
}
return this.locale;
},
localizationsDelegates: [
MyLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'), // English
const Locale('ar', ''), // Arabic
],
home: HomeScreen()
);
}
} _fetchLocale() async {
var prefs = await SharedPreferences.getInstance(); if (prefs.getString('language_code') == null) {
return null;
} return Locale(prefs.getString('language_code'),
prefs.getString('country_code'));
}
}
- Note that
_fetchLocale
now might return null if it didn’t find anything in the shared preferences - the
build
method used to check whether locale is null to determine whether to show a loading indicator or not. However, the locale now might be null even after it is loaded, so we use a boolean variable that we calllocaleLoaded
to let us know when the locale has been successfully loaded. - the
localeResolutionCallback
checks the locale instance, if it finds it null, it assigns thedeviceLocale
to it instead, and then returns it
Conclusion
And that’s it! we now have an app where we can:
- Fetch the locale from the shared preferences or from anywhere we want
- Assign this locale to our app, or assign the device locale to the app if we failed to fetch the locale
- Show the chosen locale to the user, and allow the user to change it when desired
I hope that this has been helpful, please let me know what you think!