In the settings page of my app, I would like to add an option that controls the app language.
I can set the language before starting the app like this:
#override
Widget build(BuildContext context) {
return MaterialApp(
// other arguments
locale: Locale('ar'),
);
}
But is it possible to change the language without restarting the app?
If you want to change app language without restarting the app and also without any plugin, you can follow the bellow steps:
In main file of the application, change the default MyHomePage to a StatefullWidget, in StatefullWedget for example MyHomePage create a static method setLocal as follow
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
static void setLocale(BuildContext context, Locale newLocale) async {
_MyHomePageState state = context.findAncestorStateOfType<_MyHomePageState>();
state.changeLanguage(newLocale);
}
#override
_MyHomePageState createState() => _MyHomePageState();
}
where _MyHomePageState is the state of your MyHomePage widget
In your state create a static method changeLanguage:
class _MyHomePageState extends State<MyHomePage> {
Locale _locale;
changeLanguage(Locale locale) {
setState(() {
_locale = locale;
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Afghanistan',
theme: ThemeData(primaryColor: Colors.blue[800]),
supportedLocales: [
Locale('fa', 'IR'),
Locale('en', 'US'),
Locale('ps', 'AFG'),
],
locale: _locale,
localizationsDelegates: [
AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
localeResolutionCallback: (locale, supportedLocales) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode &&
supportedLocale.countryCode == locale.countryCode) {
return supportedLocale;
}
}
return supportedLocales.first;
},
initialRoute: splashRoute,
onGenerateRoute: Router.generatedRoute,
);
}
}
Now from pages of your application you can change the language by calling the setLocal method and pass a new Locale as follow:
Locale newLocale = Locale('ps', 'AFG');
MyHomePage.setLocale(context, newLocale);
Please remember you need to create a LocalizationDelegate,
Here is the link to the Written Tutorial and Demo Application
Wrap your MaterialApp into a StreamBuilder which will be responsible for providing the Locale value to your application. And it will enable you to dynamically change it without restarting your app. This is an example using the rxdart package to implement the stream:
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: setLocale,
initialData: Locale('ar',''),
builder: (context, localeSnapshot) {
return MaterialApp(
// other arguments
locale: localeSnapshot.data,
);
}
);
}
Stream<Locale> setLocale(int choice) {
var localeSubject = BehaviorSubject<Locale>() ;
choice == 0 ? localeSubject.sink.add( Locale('ar','') ) : localeSubject.sink.add( Locale('en','') ) ;
return localeSubject.stream.distinct() ;
}
The above demonstration is just a basic way of how to achieve what you want to, but for a proper implementation of streams in your app you should consider using app-wide BloCs, which will significantly improve the quality of your app by reducing the number of unnecessary builds.
You can wrap the MaterialApp widget with a ChangeNotifierProvider and a Consumer widgets and control the language from the model.
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => MainModel(context: context),
child: Consumer<MainModel>(builder: (context, mainModel, child) {
return MaterialApp(
locale: Locale(mainModel.preferredLanguageCode),
....
On the MainModel, all you need to do is change the preferredLanguageCode variable to whatever you want ('en', 'ar', 'es', etc). Don't forget to call NotifyListeners() once you change the language.
This and the other answer have only one problem: Any context above MaterialApp can't get the device language (for example when the app is started for the first time) with Localizations.localeOf(context). This method required a context bellow MaterialApp.
To fix this issue, I used this plugin to get the device language without the need of a context.
Once the app starts, you can change the language any way you want that this approach will work. I also use SharedPreferences to store the preferred language once the user changes it.
It's easier to use easy_localization package.
For changing language, for example:
onTap: (){
EasyLocalization.of(context).locale = Locale('en', 'US');
}
I learned using this package by this video: Youtube Video Link
UPDATE:
In version 3.0.0:
EasyLocalization.of(context).setLocale(Locale('en', ''));
You can use the most popular GetX library as well.
Call Get.updateLocale(locale) to update the locale. Translations then automatically use the new locale.
var locale = Locale('en', 'US');
Get.updateLocale(locale);
Related
I am really out of options here. I am currently trying to have an option in my settings that enable the user to change language on a Button tap with Provider.
Changing the language currently works just fine, but it is not persisted, because I am passing the provider locale to the locale in my MaterialApp:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: providers,
child: ChangeNotifierProvider(
create:(_) => LocaleProvider(),
builder: (context, child) {
final provider = Provider.of<LocaleProvider>(context, listen: true);
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: L10n.supportedLocales,
locale: provider.locale,
);
},
),
);
}
}
My locale Provider currently looks like this:
class LocaleProvider extends ChangeNotifier {
Locale _locale = const Locale('de');
Locale get locale => _locale;
void setLocale(Locale locale) async {
_locale = await saveLocale(locale.languageCode);
notifyListeners();
}
The setLocale method is called from my Settings. I tried using sharedPreferences to store my locale, the Problem I have here is that I cannot call an async method on the locale property in the MaterialApp to await the value stored in sharedPreferences.
What I want to achieve is, that the user can click on the button, the language gets changed instantly and is then saved and persisted until the user changes it again or uninstalls the app.
Any help would be much appreciated here.
simple solution...
before you run the app in main.dart initiate the active local
//get active locale from persisted memory
Locale persistedLocale = //get from persisted memory
//set for provider usage
LocaleProvider().setLocale(persistedLocale);
//run the app
runApp(MyApp());
Of course, after posting this question I found a solution myself...
I am currently retrieving the SharedPreferences in my main function, and then passing them down to MyApp:
final prefs = await SharedPreferences.getInstance();
runApp(MyApp(prefs: prefs,));
In the MaterialApp, locale is set as follows:
locale: Locale(_prefs.getString('locale') ?? provider.locale.languageCode),
This will ensure, that on App Startup the locale isnt null, because in the provider, the locale property is initialized with a default value.
I'm using Localizations in my app based on the flutter documentation.
See here: https://flutter.dev/docs/development/accessibility-and-localization/internationalization
I use get_it package (version 4.0.4) to retrieve singleton objects like the Localization delegate. Unfortunately it needs a BuildContext property. Sometimes in my app I don't have the context reference so it would be nice if it would work like this: GetIt.I<AppLocalizations>() instead of this: AppLocalizations.of(context). It still can be achieved without a problem if you setup get_it like this: GetIt.I.registerLazySingleton(() => AppLocalizations.of(context)); The problem is that you need the context at least once to make it work. Moreover if you would like to display a localized text instantly in your initial route it's more difficult to get a properly initialized BuildContext at a time when you need it.
It's a little hard for me to explain it properly so I recreated the issue in a minimal example.
I commented out some code that would cause compile time errors, but it shows how I imagined it to be done.
main.dart
GetIt getIt = GetIt.instance;
void setupGetIt() {
// How to get BuildContext properly if no context is available yet?
// Compile time error.
// getIt.registerLazySingleton(() => AppLocalizations.of(context));
}
void main() {
setupGetIt();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
// The above line also won't work. It has BuildContext but Applocalizations.of(context) won't work
// because it's above in the Widget tree and not yet setted up.
getIt.registerLazySingleton(() => AppLocalizations.of(context));
return MaterialApp(
supportedLocales: const [
Locale('en', 'US'),
Locale('hu', 'HU'),
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
localeResolutionCallback: (locale, supportedLocales) {
// check if locale is supported
for (final supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale?.languageCode &&
supportedLocale.countryCode == locale?.countryCode) {
return supportedLocale;
}
}
// if locale is not supported then return the first (default) one
return supportedLocales.first;
},
// You may pass the BuildContext here for Page1 in it's constructor
// but in a more advanced routing case it's not a maintanable solution.
home: Page1(),
);
}
}
Initial route
class PageBase extends StatelessWidget {
final String title;
final Widget content;
PageBase(this.title, this.content);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: content,
);
}
}
class Page1 extends PageBase {
// It won't run because I need the context but clearly I don't have it.
// And in a real app you also don't want to pass the context all over the place
if you have many routes to manage.
Page1(String title)
: super(AppLocalizations.of(context).title, Center(child: Text('Hello')));
// Intended solution
// I don't know how to properly initialize getIt AppLocalizations singleton by the time
// it tries to retrieve it
Page1.withGetIt(String title)
: super(getIt<AppLocalizations>().title, Center(child: Text('Hello')));
}
locales.dart
String globalLocaleName;
class AppLocalizations {
//AppLocalizations(this.localeName);
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
static Future<AppLocalizations> load(Locale locale) async {
final String name =
locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final String localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
globalLocaleName = localeName;
return AppLocalizations();
});
}
String get title => Intl.message(
'This is the title.',
name: 'title',
);
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
// This delegate instance will never change (it doesn't even have fields!)
// It can provide a constant constructor.
const _AppLocalizationsDelegate();
#override
bool isSupported(Locale locale) {
return ['en', 'hu'].contains(locale.languageCode);
}
#override
Future<AppLocalizations> load(Locale locale) => AppLocalizations.load(locale);
#override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
And some intl generated dart code and .arb files that is not so important to illustrate the problem.
So all in all, how can I achive to use my AppLocalizations class as a singleton without using a context for example in a situation like this? Maybe my initial approach is bad and it can be done in other ways that I represented. Please let me know if you have a solution.
Thank you.
To achieve what you have described you need to first make the navigation service using get_it. Follow these steps to achieve the result :
1. Create a navigation service
import 'package:flutter/material.dart';
class NavigationService {
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState!
.push(routeName);
}
goBack() {
return navigatorKey.currentState!.pop();
}
}
This allows you to navigate anywhere from any point throughout the app without build context. This navigator key is what you can use to achieve the AppLocalization instance for the current context.
Refer to the FilledStacks tutorials for this method of navigating without build context.
https://www.filledstacks.com/post/navigate-without-build-context-in-flutter-using-a-navigation-service/
2. Register
GetIt locator = GetIt.instance;
void setupLocator() {
...
locator.registerLazySingleton(() => NavigationService());
...
}
3. Assign the navigator key in the material app
return MaterialApp(
...
navigatorKey: navigationService.navigatorKey,
...
),
3. Create an instance for the AppLocalizations and import it wherever you want to use
localeInstance() => AppLocalizations.of(locator<NavigationService>().navigatorKey.currentContext!)!;
3. The actual use case
import 'package:{your_app_name}/{location_to_this_instace}/{file_name}.dart';
localeInstance().your_localization_variable
You can add a builder to your MaterialApp and setup the service locator inside it with the context available. Example:
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, widget) {
setUpServiceLocator(context);
return FutureBuilder(
future: getIt.allReady(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return widget;
} else {
return Container(color: Colors.white);
}
});
},
);
}
Service Locator Setup:
void setUpServiceLocator(BuildContext context) {
getIt.registerSingleton<AppLocalizations>(AppLocalizations.of(context));
}
You could use some non-localizable splash screen with FutureBuilder and getIt.allReady().
Something like:
class SplashScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: getIt.allReady(),
builder: (context, snapshot) {
if (snapshot.hasData) {
// Navigate to main page (with replace)
} else if (snapshot.hasError) {
// Error handling
} else {
// Some pretty loading indicator
}
},
);
}
I'd like to recommend the injectable package for dealing with get_it also.
I'm learning the state management approache called Provider & Scope Model.
I made an example and its working fine.
In my example I have a list of entries and a button "+" to add a new entry.
Both views have their own routes, as shown bellow:
static Widget _buildRoute({
#required BuildContext context,
#required String routeName,
Object arguments,
}) {
switch (routeName) {
case Login:
return LoginScreen();
case OccurrenceentriesRoute:
return OccurrenceEntries();
case OccurrenceFormRoute:
Occurrence occurrence = arguments as Occurrence;
return OccurrenceForm(occurrence: occurrence);
default:
throw 'Route $routeName is not defined';
}
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => OccurrenceProvider()..loadOccurrences(),
child: MaterialApp(
title: 'Mapify',
theme: ThemeData(
primarySwatch: Colors.blue,
),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute(
builder: (BuildContext context) => Routes.makeRoute(
context: context,
routeName: settings.name,
arguments: settings.arguments,
),
maintainState: true,
fullscreenDialog: false,
);
},
),
);
}
}
After reading the flutter documentation about the Provider approach I thought that would be a better idea to place the ChangeNotifierProvider as down as possible in the widget tree, as the documentation says:
You don’t want to place ChangeNotifierProvider higher than necessary (because you don’t want to pollute the scope)
My first attempt was to use the ChangeNotifierProviders in my buildRoute, adding only the providers that i needed in each route. I did this, but the notifications made on one route don't affect the others... So, I'm really confuse, where should I place this ChangeNotifierProviders other than in the top of the widget tree?
agree you really shouldn't place your change notifier at the top because that will rebuild the whole app instead
use Multiprovider and wrap it to the very top of you widget tree
like... MaterialApp(
child:MultiProvider(
providers:[],
child:yourWidget()
));
then you can access it by final var _sampleProvider = Provider.of<SomeModel>(context);
I suggest reading more into this if this explanation isn't still clear.
Context:
Building my first Flutter app
It needs to be localized and I am not using intl package, but using the guidelines for LocalizationsDelegates (code below)
I want to load the localizations remotely from a server.
I want when my app starts to show a circular progress indicator right away, which stays until the copy has been loaded remotely fully and then the loading indicator to be dismissed.
I use provider for state management.
... and I struggle to do that! :)
Here is my main and starting point stripped out of unnecessary lines to the Q:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (BuildContext context) => 'MyTitle',
localizationsDelegates: [
const CopyDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('bg', ''),
],
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => Init(),
},
);
}
}
Here is the CopyDelegates class:
class CopyDelegate extends LocalizationsDelegate<Copy> {
const CopyDelegate();
//TODO change the source of supported languages
#override
bool isSupported(Locale locale) => ['en', 'bg'].contains(locale.languageCode);
#override
Future<Copy> load(Locale locale) async{
Copy copy = Copy(locale);
copy.load();
return copy;
}
#override
bool shouldReload(CopyDelegate old) => false;
}
Copy is a simple class that doesn't inherit and in the copy.load() method I have my logic that fetches the localizations remotely. It's an async function with lots of 'await' statements that gets to the Remote Config, gets the copy URL, then downloads it from Firestore and parses it as a JSON string that is loaded into a Map. Copy then provides methods to retrieve the loaded strings.
Finally, here is the 'Init' widget stripped of other stuff:
class Init extends StatelessWidget {
#override
Widget build(BuildContext context) {
//TODO add logic to transition away from spinner when copy has loaded
return CircularProgressIndicator();
}
}
The main issue I have is:
How do I notify 'Init' widget once my copy state has changed, i.e. when 'copy.load()' has finished all its async calls, so that I can transition away from the loading spinner?
I followed the explanations given in the official Flutter pages (see here) to make my application work in different languages.
According to the documentation, it retrieves the user's locale and this works fine.
Let's now suppose that my application supports different languages (such as EN, FR, ES, ...) and that the user could select one of these languages to use the application (the selected language would then be different than the one defined in the phone's settings), how can I achieve this?
How may I force the application Locale and dynamically "reload" all the translations?
The Flutter page does not explain this and I haven't seen anything that help me in the documentation...
Here is the current implementation:
class Translations {
Translations(this.locale);
final Locale locale;
static Translations of(BuildContext context){
return Localizations.of<Translations>(context, Translations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'title': 'Hello',
},
'fr': {
'title': 'Bonjour',
},
'es': {
'title': 'Hola',
}
};
String text(String key){
return _localizedValues[locale.languageCode][key] ?? '** ${key} not found';
}
}
class TranslationsDelegate extends LocalizationsDelegate<Translations> {
const TranslationsDelegate();
#override
bool isSupported(Locale locale) => ['en', 'fr','es'].contains(locale.languageCode);
#override
Future<Translations> load(Locale locale) {
return new SynchronousFuture<Translations>(new Translations(locale));
}
#override
bool shouldReload(TranslationsDelegate old) => false;
}
In the main.dart:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: Translations.of(context).text('title'),
theme: new ThemeData(
primarySwatch: Colors.blue,
),
localizationsDelegates: [
const TranslationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('fr', ''), // French
const Locale('fr', ''), // French
],
home: new LandingPage(),
);
}
}
Many thanks for your help.
This can be accomplished by
creating a new LocalizationsDelegate that either translates to a
single locale or defers completely depending on a parameter
converting the base app (MyApp) to a stateful widget and inserting the new delegate above into the localizationsDelegates list
managing the base app (MyApp) state with a new delegate targeting a specific locale based on some event
A simple implementation for 1) might be:
class SpecifiedLocalizationDelegate
extends LocalizationsDelegate<Translations> {
final Locale overriddenLocale;
const SpecifiedLocalizationDelegate(this.overriddenLocale);
#override
bool isSupported(Locale locale) => overriddenLocale != null;
#override
Future<Translations> load(Locale locale) =>
Translations.load(overriddenLocale);
#override
bool shouldReload(SpecifiedLocalizationDelegate old) => true;
}
Next for 2) and 3), convert the MyApp to stateful and include the new delegate (initially just deferring everything), plus some event handlers to change the state with a new delegate that specifies a new Locale.
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
SpecifiedLocalizationDelegate _localeOverrideDelegate;
#override
void initState() {
super.initState();
_localeOverrideDelegate = new SpecifiedLocalizationDelegate(null);
}
onLocaleChange(Locale l) {
setState(() {
_localeOverrideDelegate = new SpecifiedLocalizationDelegate(l);
});
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
localizationsDelegates: [
_localeOverrideDelegate,
const TranslationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('fr', ''), // French
],
home: new LandingPage(onLocaleSwitch: onLocaleChange),
);
}
}
With these changes, in children widgets you could now use Translations.of(context).myLocalizedString to retrieve the translations.
More complete gist: https://gist.github.com/ilikerobots/474b414138f3f99150dbb3d0cc4cc721
To control the locale of the app, you can use the locale property of the MaterialApp:
return MaterialApp(
...
locale: _myLocal,
...
);
This, combined with #ilikerobots StatefulWidget approach shall provide you with what you need.
using one of the Providers should do the job, I am not really familiar with providers but this got me working easily
wrap your material app using ChangeNotifierProvider
return ChangeNotifierProvider(
create: (_) => new LocaleModel(),
child: Consumer<LocaleModel>(
builder: (context, provider, child) => MaterialApp(
title: 'myapp',
locale: Provider.of<LocaleModel>(context).locale
...
...
...
create A model class with getters and setters to get & set the locale as\
import 'package:iborganic/const/page_exports.dart';
class LocaleModel with ChangeNotifier {
Locale locale = Locale('en');
Locale get getlocale => locale;
void changelocale(Locale l) {
locale = l;
notifyListeners();
}
}
Change the locale on some event (button click) as
Provider.of<LocaleModel>(context).changelocale(Locale("kn"));
The benefit of wrapping the material app within Provider is you can have access to the locale value from any part of your app
The 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
return MaterialApp(
locale: Locale('ar', ''),
localizationsDelegates: [
MyLocalizationsDelegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English
const Locale('ar', ''), // Arabic
],
home: HomeScreen()
);
This tutorial explained it better
It also explained how to load the locale preference from sharedPreferences