Flutter: how to translate tabs on BottomNavigationBarItem - flutter

I'm new to Flutter (and the Dart programming language) and I'm struggling with translating the tabs on the BottomNavigationBarItem.
I'm currently basing my code heavily on Andrea Bizzotto's Bottom Navigation Bar with Multiple Navigators.
Here's what I have so far:
Setup:
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CovidSafe extended',
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const <Locale>[
Locale('nl'),
Locale('fr'),
Locale('de'),
Locale('en'),
],
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const App(),
);
}
}
class App extends StatefulWidget {
const App({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<App> {
var _currentTab = TabItem.red;
final _navigatorKeys = {
TabItem.red: GlobalKey<NavigatorState>(),
TabItem.green: GlobalKey<NavigatorState>(),
TabItem.blue: GlobalKey<NavigatorState>(),
};
...
}
It uses a TabItem object, which is an enumerable, which has 2 properties: tabName and activeTabColor:
enum TabItem { red, green, blue }
const Map<TabItem, String> tabName = {
TabItem.red: 'red',
TabItem.green: 'green',
TabItem.blue: 'blue',
};
const Map<TabItem, MaterialColor> activeTabColor = {
TabItem.red: Colors.red,
TabItem.green: Colors.green,
TabItem.blue: Colors.blue,
};
The navbar items get built from the _buildItem function inside the BottomNavigation class, like so:
class BottomNavigation extends StatelessWidget {
const BottomNavigation(
{Key? key, required this.currentTab, required this.onSelectTab})
: super(key: key);
final TabItem currentTab;
final ValueChanged<TabItem> onSelectTab;
#override
Widget build(BuildContext context) {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
_buildItem(TabItem.red, context),
_buildItem(TabItem.green, context),
_buildItem(TabItem.blue, context),
],
onTap: (index) => onSelectTab(
TabItem.values[index],
),
currentIndex: currentTab.index,
selectedItemColor: activeTabColor[currentTab]!,
);
}
BottomNavigationBarItem _buildItem(TabItem tabItem, BuildContext context) {
return BottomNavigationBarItem(
icon: Icon(
Icons.layers,
color: _colorTabMatching(tabItem),
),
// I think this is where I need to somehow translate the tabName
label: tabName[tabItem],
);
}
Color _colorTabMatching(TabItem item) {
return currentTab == item ? activeTabColor[item]! : Colors.grey;
}
}
I've tried using the following code to translate the label/tabName:
// Option 1:
// won't work because "there's no getter for tabName"
label: AppLocalizations.of(context)!.tabName[tabItem],
// Option 2:
// tried some variants of this, this will just print "AppLocalizations.of..." as a string
label: '$AppLocalizations.of($context)!.$tabName[$tabItem]',
// Option 3:
label: AppLocalizations.of(context)!.bottomNavBarLabel(tabName[tabItem])
// and added the following to my `app_en.arb` file:
"bottomNavBarLabel": "{label}",
"#bottomNavBarLabel": {
"type": "text",
"placeholders": {
"label": {}
}
},
// but that doesn't actually translate the variable passed along (which does makes sense, but I figured I'd at least try it)
I've also tried passing the BuildContext along to the TabName Map, but couldn't get it to work.
I'm sure this is something relatively simple that I just can seem to figure out. Maybe working with a Map is actually not the way to go for this, I don't know...
So how do I make it so that the label (tabName[tabItem]) is translatable?

In my personal experience so far, you don't change the labels through passing data between widgets (not that you can't, but IMHO this is much more easier and pragmatic, since the user can have an app in his native language, if you support it that is, without having to change the apps language inside the app settings), but with locale language that is detected through app localization/internationalization.
You might wanna try a few things. First, try adding this callback in your MaterialApp() :
UPDATE: I have tested it in my emulator and it is working, updated asnwer is below.
// here you define the languages that you want to support, so it is important to first put the language you wish your app to be in
supportedLocales: const <Locale>[
Locale('en'),
Locale('nl'),
],
localeResolutionCallback: (locale, supportedLocales) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode &&
supportedLocale.countryCode == locale.countryCode) {
return supportedLocale;
}
}
return supportedLocales.first; // because of this it will always return the first locale from the list
},
Here is where you check if the current locale is supported in your app, if it is, change the text to your locale.
If you haven't yet, in your lib folder create a new folder called l10n, and add a file app_en.arb where you can define the text you wanna change in English. Then do the same with another file, I made one for German, so app_de.arb. In those files, define the text you wish to change throughout the app with locale language change:
app_en.arb
{
"##locale": "en",
"hello": "Hello",
"#hello": {
"description": "The conventional newborn programmer greeting"
},
}
You do the same for German (or any other language for that matter) but you only define the main EN you wish to change:
{
"##locale": "de",
"hello": "Hallo", // from English to German
}
Then you define where is the text you want to change with:
AppLocalizations.of(context).hello // here you define the word you want to change on app locale change
UPDATE: You are not passing context to the AppLocalizations.of(context).hello that it why it is not working for you and also, you have made your Map<TabItem, String> tabName a const and since you are switching values of the string, it can't be a const value.
// without a const value and you must put it in a separate class
//or in the class in which you are using it so you can pass the context
Map<TabItem, String> tabName = {
TabItem.red: AppLocalizations.of(context).red,
TabItem.green: AppLocalizations.of(context).green,
TabItem.blue: AppLocalizations.of(context).blue,
};
If this doesn't work, let me know and I will try to find another solution for you. Cheers!

Related

initalize static string member in dart with a localized value using flutter_intl localization lib

I'm new to flutter and collecting fragments of code from here and there.
I have a class that holds some data that will be passed later to a widget and generate the UI based on the passed data
class OnBoardingViewModel with ChangeNotifier {
final List<OnBoardingPageContent> onBoardingPages = [
OnBoardingPageContent(
image: ImageManager.onBoardingImage1,
title: StringManager.onBoardingTitle1,// I want the text to be localized
subtitle: StringManager.onBoardingSubTitle1,
),
OnBoardingPageContent(
image: ImageManager.onBoardingImage2,
title: StringManager.onBoardingTitle2,
subtitle: StringManager.onBoardingSubTitle2,
)
];
... some other code
the strings above are hardcoded with specific language I wanted them to be localized
the localization require
S.of(context).yourKey
I thought of passing the key as a string and in UI build (where I will have the context)
to do
`S.of(context)[thePassedKeyStringVar]` // but that is wrong in Dart
I tried to pass the context to my Class OnBoardingViewModel so I could have
class OnBoardingViewModel with ChangeNotifier {
static BuildContext context;
final List<OnBoardingPageContent> onBoardingPages = [
OnBoardingPageContent(
title: S.of(context).onBoardingText1,
but it did not end well with me
here is the entry point of my app
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => OnBoardingViewModel(_),
),
...
],
child: const MyApp(),
),
);
}
any help on how to localize the strings in my static list in the above class?
Short answer : That is not possible with the library you're using (it is a strictly static-only localization library).
Detailed answer :
The way I handle translations when having to manipulate datas in a Controller/Presenter/ViewModel (call it whatever you want), is by returning the key to translate in my model.
You should be able to do what you want with the easy_localization package
/assets/translations/en.json
{[
'my_key': 'The english value',
]}
class AccountController with ChangeNotifier {
AccountObject accountObj = AccountObject();
void updateObject() {
accountObj.title = "my_key";
notifyListeners();
}
}
class AccountWidget extends StatelessWidget {
const AccountWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<AccountController>(
builder: (context, accountController, child) {
return Center(
child: Text(accountController.accountObj.title).tr
);
}
);
}
}
Another good practice is to keep your Widgets outside from your Controllers.
Use your Controllers to build the data and notify the Widget Tree when it should re-build (and keep your Widgets in your Widgets tree) :)
Also, keep in mind that your Context should stay in your Widgets (it'll save you from a lot of errors/weird behaviors :))

Flutter pass data to new screen with onTap

My application has a bottom navigation bar, with 2 pages in the menu.
On page 1, I can fill out a form and it calculates me values ​​that it displays to me by pushing in a 1.1 page.
On this page I have a button that allows me to redirect me to page 2 as if I clicked menu 2 of the navigation bar.
This works. My problem is how to send the data from my page 1.1 to this page 2.
The goal being that my page 2 is a form which is empty if I call it by the navigation bar but which is filled automatically if I pass by the page 1.1 in focus of the calculated values.
Here an exemple of the redirection that I do:
Here is my code :
my_app.dart :
final ThemeData _AppTheme = AppTheme().data;
final navBarGlobalKey = GlobalKey(); // => This is my key for redirect page
class MyApp extends StatefulWidget{
#override
State<StatefulWidget> createState() => MyAppState();
}
class MyAppState extends State<MyApp>{
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'App',
home: MyBottomNavigationBar(),
theme: _AppTheme,
navigatorKey: locator<NavigationService>().navigatorKey,
onGenerateRoute: Router.generateRoute,
initialRoute: HOME_ROUTE,
);
}
}
My bottom_navigation_bar.dart :
class MyBottomNavigationBar extends StatefulWidget
{
MyBottomNavigationBar({Key key}) : super(key: key);
#override
_MyBottomNavigationBarState createState() => _MyBottomNavigationBarState();
}
class _MyBottomNavigationBarState extends State<MyBottomNavigationBar>
{
int _pageIndex = 0;
final List<Widget> _pagesOption = [
page1.1(), // => Here I load direclty my page 1.1 with data for the exemple
page2(),
];
void onTappedBar(int index)
{
setState(() {
_pageIndex = index;
});
}
#override
Widget build(BuildContext context) {
return SafeArea(
child : Scaffold(
body : _pagesOption.elementAt(_pageIndex),
bottomNavigationBar: BottomNavigationBar(
key: navBarGlobalKey,
currentIndex: _pageIndex,
onTap: onTappedBar,
type: BottomNavigationBarType.fixed,
items : [
BottomNavigationBarItem(
icon : Icon(Icons.home),
title : Text('Home')
),
BottomNavigationBarItem(
icon : Icon(Icons.settings),
title : Text('Setting')
),
]
),
)
);
}
}
And here my widget submit button of page 1.1 :
Widget _getSubmitButton(){
return RaisedButton(
child: Text(
'Send'
),
onPressed: () {
final BottomNavigationBar navigationBar = navBarGlobalKey.currentWidget;
navigationBar.onTap(1); // => How to send data that I have in my page ???
},
);
}
For this, you can use Shared Preferences, the main idea is that:
Store the value of the calculated value in SharedPref from Page 1 when you're passing to Page 1.1
Let you checks for the value by default in Page 2's initState(), any changes in the Shared Preferences will be fetched in the Page 2 itself, using SharedPref get method.
WHY?
This is probably a cleaner way to achieve what you want, since in the BottomNavigationBar will not help you do this, but a Shared Preferences value will always give you that data which you can use it any time
Let's see how you can achieve this:
PAGE ONE
// Set the data of the form here
class _PageOneState extends State<PageOne>
{
void onSubmit() async{
SharedPreferences prefs = await SharedPreferences.getInstance();
//make sure you store the calculated value, if that is String
// use setString() or if it is an int, setInt()
// and then pass it to the SharedPref
// key is a string name, which is used to access
// key and store, so choose the name wisely
await prefs.setInt("key", your_calculated_value);
}
}
PAGE TWO
class _PageTwoState extends State<PageTwo>
{
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
// This will be responsible for getting the result from SharedPref
int calculated_value;
#override
void initState(){
super.initState();
// get your list here
calculated_value = _prefs.then((SharedPreferences prefs){
// here if no data is then _values will have 0
// which you can use it to check and populate data
return (prefs.getInt("key") ?? 0);
});
}
}
This is the most reasonable way of doing the thing which you want. In this manner, whenever, PageTwo will trace any values, it will reflect, else, your choice for 0 check result. Let me know, if you have any doubts in that.
In your FirstActivity
onPressed: () {
navigatePush(SecondActivity(text: "Data"));
}
In your SecondActivity
class SecondActivity extends StatefulWidget {
String text;
SecondActivity({this.text});
}
You can pass the the values as arguments when you push to your new screen. This could get messy if you're building a larger project.
A cleaner implementation would be to use a Provider. Set up the data you want in a model mixed in with ChangeNotifier and use Provider.of<*name of your class*>(context) where ever you need to use it.

How can I pass dynamic data from widget to widget?

Let me preface this by saying I am brand new to flutter/dart, and also not a super experienced programmer.
I'm trying to acquaint myself with flutter's framework and tools, and I'm trying to just expand upon the basic counter app that flutter creates on project generation. My goal is to have the app keep track of when the counter is 'reset', keep the time and count that the counter was at, and then display that data in a table on another screen.
Here's what I have so far:
I've made a class to keep track of the data:
class CounterRecord {
int _counter; //Holds the value the counter was at on reset
DateTime _resetTime; //Holds the time when the counter was reset
CounterRecord(int _count){
_counter = _count;
_resetTime = DateTime.now();
}
int getCount() => _counter; //fetch method for count
DateTime getTime() => _resetTime; //Fetch method for resettime
}
Here's the main class/home page:
import 'package:counter_app/clickerScreen.dart';
import 'package:counter_app/dataScreen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
final clickerKey = new GlobalKey<ClickerScreenState>();
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.deepOrange,
accentColor: Colors.grey,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home'),
);
}
}
class MyHomePage extends StatefulWidget {
//Enables the passing in of the title, clicker screen instance, and datacreen isntance, respectively,
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
//We don't want a brand new clickerScreen every time, so I'm keeping it up here.
ClickerScreen clickerScreen = ClickerScreen(clickerKey: clickerKey); //Creates a new clickerScreen - the key points to it too.
#override
Widget build(BuildContext context) {
//Creates an instance (State?) of clickerScreen for the first tab
return DefaultTabController( //A wrapper that helps manage the tab states
length: 2, //Currently there are only two options for screens
child: Scaffold(
appBar: AppBar( //This represnts the bar up at the top
title: Text(widget.title),
bottom: TabBar(
tabs: [
//These are the icons for the two tabs we're using
//The order of these is important: It goes in the same order as TabBarView below
Tab(icon: Icon(Icons.home)),
Tab(icon: Icon(Icons.directions_run)),
],
)
),
body: TabBarView(
children: [
clickerScreen,
DataScreen( //this DataScreen will be built every time based on the new data from clickerScreen
data: clickerKey.currentState.getRecords(),
),
],
),
),
);
}
}
class CounterRecord {
int _counter; //Holds the value the counter was at on reset
DateTime _resetTime; //Holds the time when the counter was reset
CounterRecord(int _count){
_counter = _count;
_resetTime = DateTime.now();
}
int getCount() => _counter; //fetch method for count
DateTime getTime() => _resetTime; //Fetch method for resettime
}
Here's the important part of my clickerScreen file:
class ClickerScreen extends StatefulWidget {
ClickerScreen({Key clickerKey}) : super(key: clickerKey);
#override
ClickerScreenState createState(){
return ClickerScreenState();
}
}
class ClickerScreenState extends State<ClickerScreen> {
int _counter = 0;
List<CounterRecord> records;
/* All three of these functions do very similar things, modify the counter value. */
void _resetCounter(){
setState(() {
records.add(CounterRecord(_counter));
_counter = 0;
});
}
List<CounterRecord> getRecords(){
return records;
}
There is a build method in clickerScreen that just displays buttons and text. I'm not assigning the key in there, as it just returns a Center widget, but I've read some things that suggest maybe I should be.
And here is my dataScreen file:
import 'package:flutter/material.dart';
import 'main.dart';
class DataScreen extends StatefulWidget{
//Enables the passing in of the instance of the clicker screen instance
DataScreen({Key key, #required this.data}) : super(key: key);
final List<CounterRecord> data;
#override
_DataScreenState createState(){
return _DataScreenState();
}
}
class _DataScreenState extends State<DataScreen>{
#override
Widget build(BuildContext context) {
return Text(widget.data.toString());
}
}
I know that it the displaying won't actually look like it's supposed to, as I'm just sending it toString(), but I want to make sure I can pass the data in before I start messing around with that.
When I run this, I get a NoSuchMethod error on getRecords(), receiver: null. I've also tried to call createState() on the ClickerScreen widget, as a last-ditch attempt.
Any advice?
(I've pasted the entire clickerScreen file here (https://pastebin.com/j6Y8M8F3) since I didn't want to make this post any longer than it already is.)
If you have two widgets depending on the same state you have to use something called "lifting state up". That means that the state is part of the closest widget that has both other widgets as children. In your case that would be the MyHomePage Widget that holds the CounterRecord List. It passes the list through the constructer to the DataScreen, and passes the onReset callback to the ClickerScreen.
MyHomePage:
class MyHomePage extends StatefulWidget {
//Enables the passing in of the title, clicker screen instance, and datacreen isntance, respectively,
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<CounterRecord> counterRecord = []; //this is the lifted up state
onReset(int count) {
setState(() {
counterRecord.add(CounterRecord(count));
});
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
//A wrapper that helps manage the tab states
length: 2, //Currently there are only two options for screens
child: Scaffold(
appBar: AppBar(
//This represnts the bar up at the top
title: Text(widget.title),
bottom: TabBar(
tabs: [
//These are the icons for the two tabs we're using
//The order of these is important: It goes in the same order as TabBarView below
Tab(icon: Icon(Icons.home)),
Tab(icon: Icon(Icons.directions_run)),
],
)),
body: TabBarView(
children: [
ClickerScreen(onReset: onReset),
DataScreen(
data: counterRecord, //pass the record data to the datascreen
),
],
),
),
);
}
}
ClickerScreen:
class ClickerScreen extends StatefulWidget {
final Function(int) onReset;
ClickerScreen({Key clickerKey, this.onReset}) : super(key: clickerKey);
#override
ClickerScreenState createState() {
return ClickerScreenState();
}
}
class ClickerScreenState extends State<ClickerScreen> {
int _counter = 0;
/* All three of these functions do very similar things, modify the counter value. */
void _resetCounter() {
widget.onReset(_counter); //call the onReset callback with the counter
setState(() {
_counter = 0;
});
}
#override
Widget build(BuildContext context) {
....
}
}
DataScreen (can be Stateless, since state is in its parent)
class DataScreen extends StatelessWidget{
//Enables the passing in of the instance of the clicker screen instance
DataScreen({Key key, #required this.data}) : super(key: key);
final List<CounterRecord> data;
#override
Widget build(BuildContext context) {
return Text(widget.data.toString());
}
}
Using this simple approach can get very annoying fast, and needs lot of changes when you move a widget in the widget tree. Thats why advanced state management like Provider with ChangeNotifier or Bloc exist.
Here is a good read on this matter::
https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
I followed Leo Letto's advice and instead used an InheritedWidget placed at the very top that held a list of records.

How to change a Flutter app language without restarting the app?

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);

Flutter: Multi-lingual application - how to override the locale?

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