To be honest, I didn't meet this kind of error before. I am following a tutorial which you can see here:
https://resocoder.com/2019/06/01/flutter-localization-the-easy-way-internationalization-with-json/
Here is my code:
MAIN.dart:
import 'app_localizations.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
supportedLocales: [Locale('en', 'US'), Locale('vi', 'VN')],
localizationsDelegates: [
// THIS CLASS WILL BE ADDED LATER
// A class which loads the translations from JSON files
AppLocalizations.delegate,
// Built-in localization of basic text for Material widgets
GlobalMaterialLocalizations.delegate,
// Built-in localization for text direction LTR/RTL
GlobalWidgetsLocalizations.delegate,
],
localeResolutionCallback: (locale, supportedLocales) {
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale!.languageCode &&
supportedLocale.countryCode == locale.countryCode) {
return supportedLocale;
}
}
return supportedLocales.first;
},
home: MyHomePage(),
);
}
}
APP_LOCALIZATIONS.dart:
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class AppLocalizations {
final Locale locale;
//error right here
AppLocalizations(this.locale);
//error right here
//modify
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
//modify
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
Map<String, String> _localizedStrings;
Future<bool> load() async {
String jsonString =
await rootBundle.loadString('lang/${locale.languageCode}.json');
Map<String, dynamic> jsonMap = json.decode(jsonString);
_localizedStrings = jsonMap.map((key, value) {
return MapEntry(key, value.toString());
});
return true;
}
//modify
String? translate(String key) {
return _localizedStrings[key];
}
}
//modify
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
#override
bool isSupported(Locale locale) {
return ['en', 'vi'].contains(locale.languageCode);
}
#override
Future<AppLocalizations> load(Locale locale) async {
AppLocalizations localizations = new AppLocalizations(locale);
await localizations.load();
return localizations;
}
#override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
Some code here is modified because of some error so it is a little different from the tutorial (to fix error.). So maybe it is the reason. I put some comment about that modification up there.
P/S: I tried searching for other answer about this but I couldn't understand much so pls explains for me!
Just initialize _localizedStrings to an empty map.
Map<String, String> _localizedStrings = {};
This way it will never be null even if it doesn't have data yet.
The other option is to add the late modifier before declaration which tells the compiler it will be initialized later on.
late Map<String, String> _localizedStrings;
Related
Can someone point out why I'm getting a null error where loading text within my localizations structure.
Error commented below at authSighInText.
The only thing I can think of is that _localizedValues is not initialized yet upon app launch. Should I be moving this earlier into main or something?
My language structure:
class AppLocalizationsDelegate extends LocalizationsDelegate<TextContent> {
const AppLocalizationsDelegate();
#override
bool isSupported(Locale locale) => TextContent.languages().contains(locale.languageCode);
#override
Future<TextContent> load(Locale locale) {
return SynchronousFuture<TextContent>(TextContent(locale));
}
#override
bool shouldReload(AppLocalizationsDelegate old) => false;
}
class TextContent {
TextContent(this.locale);
final Locale locale;
static TextContent of(BuildContext context) {
return Localizations.of<TextContent>(context, TextContent)!;
}
// Add language list conglomeration from TextUtility here
static final _localizedValues = <String, Map<String, String>>{
'en': TextUtility.englishTexts(),
};
static List<String> languages() => _localizedValues.keys.toList();
/* **********************************************************************************************
Infrastructure Authentication Texts
************************************************************************************************/
String get authSignInText {
return _localizedValues[locale.languageCode]!['signIn']!; // NULL ERROR HERE...???
}
String get authFormEmailText {
return _localizedValues[locale.languageCode]!['email']!;
}
String get authFormPasswordText {
return _localizedValues[locale.languageCode]!['password']!;
}
...
}
Maps:
abstract class TextUtility {
///
/// All english text maps to be manually hardcoded into this list.
static List<Map<String, String>> englishTextLists = [
InfrastructureAuthenticationText.authenticationText,
InfrastructureFormValidationErrorText.invalidEmailText,
InfrastructureDialogMessagesText.dialogMessagesText,
];
///
/// English texts.
///
/// Method conglomerates all texts from above english files into one map. This method is called
/// within LanguageLocalizations TextContent.
///
/// Returns:
/// [Map<String, String>]
static Map<String, String> englishTexts() {
Map<String, String> allEnglishTexts = {};
for (int i = 0; i < englishTextLists.length; i++) {
allEnglishTexts.addAll(englishTextLists[i]);
}
return allEnglishTexts;
}
}
Language texts:
abstract class InfrastructureAuthenticationText {
static const Map<String, String> authenticationText = {
// Account actions
'sign_in': 'Sign In',
'sign_out': 'Sign Out',
'sign_up': 'Sign Up',
'sign_up punctuated': 'Sign Up.',
...
};
}
Where text call is made:
Text(
TextContent.of(context).authSignInText,
style: ThemeEndpoints.primaryHeader(),
),
You need to do that :
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await EasyLocalization.ensureInitialized();
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]).then((_) {
runApp(EasyLocalization(
supportedLocales: [Locale('en', 'US'), Locale('bn', 'BN')],
path: 'lib/asset/translations',
fallbackLocale: Locale('en', 'US'),
saveLocale: true,
child: MyApp()));
});
}
I've created some lib in my project. There are a lot of words and phrases, so I must translate everything.
I set the app's language using bloc but I don't know how to use it in my custom library.
I think I should use something related to context. I don't know at all.
Help me, please.
It's my class for localizations.
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'constants/constants.dart';
class AppLocalizations {
Locale locale;
AppLocalizations(this.locale);
/// Helper method to keep the code in the widgets concise
/// Localizations are accessed using an InheritedWidget "of" syntax
static AppLocalizations of(BuildContext context) {
return Localizations.of(context, AppLocalizations);
}
/// Static member to have a simple access to the delegate from the MaterialApp
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
late Map<String, String> _localizedStrings;
Future<bool> load() async {
String jsonString =
await rootBundle.loadString('assets/langs/${locale.languageCode}.json');
Map<String, dynamic> jsonMap = json.decode(jsonString);
_localizedStrings = jsonMap.map((key, value) {
return MapEntry(key, value.toString());
});
return true;
}
String? translate(String key) {
return _localizedStrings[key];
}
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
// add all languages code here
#override
bool isSupported(Locale locale) {
return LanguageKeys.LANGUAGE_KEYS_LIST.contains(locale.languageCode);
}
// load all localization files
#override
Future<AppLocalizations> load(Locale locale) async {
AppLocalizations localizations = new AppLocalizations(locale);
await localizations.load();
return localizations;
}
#override
bool shouldReload(LocalizationsDelegate<AppLocalizations> old) => false;
}
I don't know much about bloc pattern since I have always used provider.
But the basic step would be:
(1) Inject the AppLocalizations class localization into material app
MaterialApp(
supportedLocales: [
Locale('en'),
Locale('es'),
],
localizationsDelegates: [
AppLocalizations.delegate,
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;
},
locale: Locale('en'), // translate to this language
.......
);
(2) For any translation pass the key to Applocalization
AppLocalizations.of(context).translate("understood")
You can change translate method in AppLocalizations class
(3) If you want to change app locale/lang add your stateful variable to locale key in Material App and put the Consumer above MaterialApp itself.
ChangeNotifierProvider<LangSettingsProvider>(
create: (_) => LangSettingsProvider(),
child: Consumer<LangSettingsProvider>(
builder: (context, lang, _) {
return MaterialApp(
locale : lang.appLocal
....
(4) Include assets inside assets folder and specify in the pubspec yml file
assets:
- assets/icon/
- assets/lang/en.json
- assets/lang/es.json
(5) Sample of the json key value pair
//en.json
"understood":"Understood",
//es.json
"understood":"Entendido",
I am new to flutter.
I am building a multi language app.
Before app start it needs to load current locale file.
Then every time user changes the locale it needs to load the new file.
At least in theory, I think this can be done using "ChangeNotifierProvider" , ProxyProvider or something similar.
So I have AppLanguage class load the correct locale file based on language code
class AppLanguage extends ChangeNotifier {
String _appLocale = 'en';
Map<String, String> _localizedStrings;
Map<String, String> get localeData => this._localizedStrings;
Future<bool> getLocaleData() async {
var prefs = await SharedPreferences.getInstance();
if (prefs.getString('language_code') == null) {
_appLocale = 'en';
await prefs.setString('language_code', _appLocale);
} else {
_appLocale = prefs.getString('language_code');
}
String jsonString = await rootBundle.loadString('i18n/$_appLocale.json');
Map<String, dynamic> jsonMap = json.decode(jsonString);
_localizedStrings = jsonMap.map((key, value) {
return MapEntry(key, value.toString());
});
return true;
}
Future<void> changeLanguage(String locale) async {
var prefs = await SharedPreferences.getInstance();
_appLocale = locale;
await prefs.setString('language_code', locale);
notifyListeners();
}
}
getLocaleData() function read the data and changeLanguage change current locale and fires notifyListeners
class Translator {
final Map<String, String> localizedStrings;
Translator(this.localizedStrings);
String translate(String key) {
return localizedStrings[key];
}
}
the Widgets will use translator class to get the correct translations.
The problem I have is, how to wire this up in main. I am stuck at how to setup the providers.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
AppLanguage appLanguage = AppLanguage();
await appLanguage.getLocaleData();
runApp(MyApp(appLanguage: appLanguage));
}
class MyApp extends StatelessWidget {
final AppLanguage appLanguage;
MyApp({this.appLanguage});
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ProxyProvider<AppLanguage, Translator>(
update: (context, appLanguage, trans) =>
Translator(appLanguage.localeData),
),
],
child: MaterialApp(
title: 'Language Demo',
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Hello'),
),
body: Container(),
);
}
}
Can someone kindly provide some help to wire this up?
Or maybe provide a better way of doing this?
A few things, ProxyProvider (or any type of ProxyProvider, ChangeNotifierProxyProvider, etc.) updates its value when the provider it depends on changes too, but you created the AppLanguage in main, without an inject dependency, just like a simple class (it's not really provided in the context) so it would be easier to just use a ChangeNotifierProvider in this case.
There is a parameter called window.locale that return the language the device it's using at that time, at the start of the app you can use it to know the language of the device if you're don't have it the sharedPreference the first time. Advante of this it's that in your example if there is not preference saved it will use the default 'en' for English, but you also support japanase, so if someone has its device in japanese and download your app for the first time it would be nice to use japanese since the beginning.
Future<Locale> _getLocaleData() async {
var prefs = await SharedPreferences.getInstance();
String languageCode = prefs.getString('language_code');
if (languageCode == null) {
return window.locale;
} else {
return Locale(languageCode);
}
}
void main() async {
Locale locale = await _getLocaleData();
runApp(MyApp(
appLanguage: locale,
));
}
class MyApp extends StatelessWidget {
final Locale language;
MyApp({this.language});
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<AppLanguage>(
builder: (_) => AppLanguage(language),
child: Consumer<AppLanguage>(builder: (context, model, _) {
return MaterialApp(
locale: model.appLocal,
supportedLocales: [
Locale('en', 'US'),
Locale('ja', ''),
],
localizationsDelegates: [
AppLocalizations.delegate, //create your AppLocalizations just like the article you shared
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: MyWidget(),
);
}),
);
}
}
class AppLanguage extends ChangeNotifier {
AppLanguage(Locale locale) : _appLocale = locale;
Locale _appLocale;
Locale get appLocal => _appLocale;
Future<void> changeLanguage(String locale) async {
var prefs = await SharedPreferences.getInstance();
_appLocale = Locale(locale);
await prefs.setString('language_code', locale);
notifyListeners();
}
}
My story in short is, I can successfully change app theme dynamically, but I fail when it comes to start my app with the last chosen ThemeData.
Here is the main.dart:
import "./helpers/constants/themeConstant.dart" as themeProfile;
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
//Several ChangeNotifierProviders
],
child: Consumer<AuthenticateProvider>(
builder: (ctx, authData, _) => ChangeNotifierProvider<ThemeChanger>(
create: (_) {
ThemeData themeToBeSet;
themeProfile.setInitialTheme().then((themeData) {
themeToBeSet = themeData;
});
return ThemeChanger(themeToBeSet);
},
child: _MaterialAppWithTheme(authData),
)
)
);}}
The problem is themeToBeSet variable always being null eventhough I set a ThemeData as I do below:
ThemeData selectedTheme;
Future<ThemeData> setInitialTheme() async {
final preferences = await SharedPreferences.getInstance();
if (!preferences.containsKey(ApplicationConstant.sharedTheme)) {
selectedTheme = appThemeDataDark;
final currentThemeInfo = json.encode({
"themeStyle": ApplicationConstant.darkAppTheme
});
preferences.setString(ApplicationConstant.sharedTheme, currentThemeInfo);
return selectedTheme;
}
else {
final extractedThemeInfo = json.decode(preferences.getString(ApplicationConstant.sharedTheme)) as
Map<String, dynamic>;
final chosenTheme = extractedThemeInfo["themeStyle"];
if (chosenTheme == ApplicationConstant.lightAppTheme) {
selectedTheme = appThemeDataLight;
return selectedTheme;
}
else if (chosenTheme == ApplicationConstant.darkAppTheme) {
selectedTheme = appThemeDataDark;
return selectedTheme;
}
else {
selectedTheme = appThemeDataDark;
return selectedTheme;
}}}
Here, I used shared_preferences.dart package to store and retrieve ThemeData info. If I debug this block, I see that my selectedTheme variable is set one of these ThemeData successfully. But, for a reason I couldn't able to find out, themeToBeSet variable on main.dart is not assigned to the result of my setInitialTheme() method.
Is it because of being asynchronous? But, isn't Dart waiting an asynchronous method with .then()?
In order not to leave any questionmarks realated for my other sections, I'm also sharing ThemeChanger class,
class ThemeChanger with ChangeNotifier {
ThemeData _themeData;
ThemeChanger(
this._themeData
);
getTheme() => _themeData;
setTheme(ThemeData theme) {
_themeData = theme;
notifyListeners();
}
}
And, _MaterialAppWithTheme,
class _MaterialAppWithTheme extends StatelessWidget {
final AuthenticateProvider authData;
_MaterialAppWithTheme(
this.authData,
);
Widget build(BuildContext context) {
final theme = Provider.of<ThemeChanger>(context);
return MaterialApp(
title: 'Game Shop Demo',
theme: theme.getTheme(),
home: authData.isLogedin ?
HomeScreen(authData.userId) :
FutureBuilder(
future: authData.autoLogin(),
builder: (ctx, authResult) => authResult.connectionState == ConnectionState.waiting ?
SplashScreen():
LoginScreen()
),
routes: {
//Several named routes
},
);
}
}
As I suspected, I misused .then().
I thought Dart is awaiting when you use .then() but after running into this post, I learnt that it is not awaiting..
So, I carry setInitialTheme() method to ThemeChanger class (it was in a different class previously) and call it in the constructor. Here its final version,
class ThemeChanger with ChangeNotifier {
ThemeData _themeData;
ThemeChanger() {
_setInitialTheme();
}
getTheme() => _themeData;
setTheme(ThemeData theme) {
_themeData = theme;
notifyListeners();
}
Future<ThemeData> _setInitialTheme() async {
final preferences = await SharedPreferences.getInstance();
if (!preferences.containsKey(ApplicationConstant.sharedTheme)) {
_themeData = appThemeDataDark;
final currentThemeInfo = json.encode({
"themeStyle": ApplicationConstant.darkAppTheme
});
preferences.setString(ApplicationConstant.sharedTheme, currentThemeInfo);
return _themeData;
}
else {
final extractedThemeInfo = json.decode(preferences.getString(ApplicationConstant.sharedTheme)) as Map<String, dynamic>;
final chosenTheme = extractedThemeInfo["themeStyle"];
if (chosenTheme == ApplicationConstant.lightAppTheme) {
_themeData = appThemeDataLight;
return _themeData;
}
else if (chosenTheme == ApplicationConstant.darkAppTheme) {
_themeData = appThemeDataDark;
return _themeData;
}
else {
_themeData = appThemeDataDark; //Its better to define a third theme style, something like appThemeDefault, but in order not to spend more time on dummy stuff, I skip that part
return _themeData;
}
}
}
}
Now, as you can see, ThemeChanger class is no longer expecting a ThemeData manually, but setting it automatically whenever its called as setInitialTheme() method is assigned to its constructor. And, of course, MyApp in main.dart is changed accordingly:
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
//Several ChangeNotifierProviders
],
child: Consumer<AuthenticateProvider>(
builder: (ctx, authData, _) => ChangeNotifierProvider<ThemeChanger>(
create: (_) => ThemeChanger(),
child: _MaterialAppWithTheme(authData),
)
)
);
}
}
Now, app is launching just fine with the last selected ThemeData which has a pointer stored in SharedPreferences.
I am implementing remember me option on the login screen, want to call shared preference before the widget is created. we have the one and only entry point that is the main function, but how we can call a function here to read primitive data (email/password).
void main() {
setupLocator();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: "/",
onGenerateRoute: Router.generateRoute,
));
}
reading bool value
Future<bool> read(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getbool(key);
}
I also try to run a asyn function before route
String firstNav;
void main() {
setupLocator();
readSharedPref();
if(firstNav!=null)
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: firstNav,
onGenerateRoute: Router.generateRoute,
));
}
void readSharedPref() async {
Utiles df=Utiles();
String isRem=await df.read("remember");
if (isRem.contains("true")) {
firstNav='homeview';
} else {
firstNav='/';
}
}
You need to set your main function as async, and add an await and a line of code:
void main() async{
//Add this lines is necessary now that your main is async
WidgetsFlutterBinding.ensureInitialized();
//Now you have to "await" the readSharedPref() function
await readSharedPref();
// And here comes all your code
}
Instead of waiting waiting for sharedPreference to load before building any widgets, just show a loader widget with progress indicator until the shared preference is loaded, and when it's loaded, show the required view based on the value loaded from sharedPreference, here is how you can modify your code, (replace HomeView and RootView widgets with your respective widgets for your homeView and / routes)
void main() {
setupLocator();
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: new ThemeData(fontFamily: 'OpenSans-Light'),
initialRoute: Loader(),
onGenerateRoute: Router.generateRoute,
));
}
class Loader extends StatefulWidget {
#override
_LoaderState createState() => _LoaderState();
}
class _LoaderState extends State<Loader> {
Widget firstNav;
#override
void initState() {
super.initState();
readSharedPref();
}
void readSharedPref() async {
Utiles df=Utiles();
String isRem=await df.read("remember");
if (isRem.contains("true")) {
setState(() {
// firstNav='homeview';
firstNav=HomeView(); // replace HomeView with the widget you use for homeview route
});
} else {
setState(() {
// firstNav='/';
firstNav=RootView(); // replace RootView with the widget you use for / route
});
}
}
#override
Widget build(BuildContext context) {
return firstNav != null ? firstNav : Center(child: CircularProgressIndicator(),);
}
}