Getx offers many convenient features for state managment. It also features about Localization or multiple language support.
It has great support for loading json data for multiple language app. We would be using locale, translations and fallbacklocale properties.
The above three properties helps to realize building multiple language app using Getx.
We will cover the below things here
JSON Files
Make sure that you have json files for language information in your assets folder. Here's en.json
{
"select_language": "Select Language",
"save": "Save",
"select_a_language": "Select a language",
"home": "Home",
"back_to_home": "back to home"
}
And where's bn.json
{
"select_language": "একটি ভাষা নির্বাচন করুন",
"save": "সংরক্ষণ",
"select_a_language": "একটি ভাষা নির্বাচন করুন",
"home": "বাড়ি",
"back_to_home": "বাড়িতে ফিরে যাও"
}
You can use any kind of language, just make sure your json files may correct format and translation.
App Constants
Let's define our app constants file. We will need for storing data to sharedPreferences. Our loaded JSON files would be stored in sharedPreference
class AppConstants {
/*
Localization data
*/
static const String COUNTRY_CODE = 'country_code';
static const String LANGUAGE_CODE = 'language_code';
static List<LanguageModel> languages = [
LanguageModel(imageUrl: "xx", languageName: 'English', countryCode: 'US', languageCode: 'en'),
LanguageModel(imageUrl: "xx", languageName: 'বাংলা', countryCode: 'BD', languageCode: 'bn'),
];
}
Here we have String COUNTRY_CODE and LANGUAGE_CODE for storing data. Here we also instantiate two objects for our languages using LanguageModel.
At the same we return them in a List so that we can access them later from dependency injection file.
Language Model
Let's first create Language model class. It will save
class LanguageModel {
String imageUrl;
String languageName;
String languageCode;
String countryCode;
LanguageModel({
required this.imageUrl,
required this.languageName,
required this.countryCode,
required this.languageCode});
}
Getx Dependencies
Next let's create a class load our dependencies. This dependency file will load Localization Controller, SharedPreference and JSON data from local device.
Future<Map<String, Map<String, String>>> init() async {
final sharedPreference = await SharedPreferences.getInstance();
Get.lazyPut(() => sharedPreference);
Get.lazyPut(() => LocalizationController(sharedPreferences: Get.find()));
// Retrieving localized data
Map<String, Map<String, String>> _languages = Map();
for(LanguageModel languageModel in AppConstants.languages) {
String jsonStringValues = await rootBundle.loadString('assets/language/${languageModel.languageCode}.json');
Map<String, dynamic> _mappedJson = json.decode(jsonStringValues);
Map<String, String> _json = Map();
_mappedJson.forEach((key, value) {
_json[key] = value.toString();
});
_languages['${languageModel.languageCode}_${languageModel.countryCode}'] = _json;
}
return _languages;
}
Here we access the List that we created before in App Constants file. We acess them AppConstants.languages inside for loop.
Localization Controller
Let's create actual controller. This controller be responsible for doing the below things
class LocalizationController extends GetxController implements GetxService {
final SharedPreferences sharedPreferences;
LocalizationController({required this.sharedPreferences}) {
loadCurrentLanguage();
}
Locale _locale = Locale(AppConstants.languages[0].languageCode,
AppConstants.languages[0].countryCode);
bool _isLtr = true;
List<LanguageModel> _languages = [];
Locale get locale => _locale;
bool get isLtr => _isLtr;
List<LanguageModel> get languages => _languages;
void setLanguage(Locale locale) {
Get.updateLocale(locale);
_locale = locale;
if (_locale.languageCode == 'bn') {
_isLtr = false;
} else {
_isLtr = true;
}
saveLanguage(_locale);
update();
}
void loadCurrentLanguage() async {
_locale = Locale(sharedPreferences.getString(AppConstants.LANGUAGE_CODE) ??
AppConstants.languages[0].languageCode,
sharedPreferences.getString(AppConstants.COUNTRY_CODE) ??
AppConstants.languages[0].countryCode);
_isLtr = _locale.languageCode != 'bn';
for (int index = 0; index < AppConstants.languages.length; index++) {
if (AppConstants.languages[index].languageCode == _locale.languageCode) {
_selectedIndex = index;
break;
}
}
_languages = [];
_languages.addAll(AppConstants.languages);
update();
}
void saveLanguage(Locale locale) async {
sharedPreferences.setString(
AppConstants.LANGUAGE_CODE, locale.languageCode);
sharedPreferences.setString(AppConstants.COUNTRY_CODE, locale.countryCode!);
}
int _selectedIndex = 0;
int get selectedIndex => _selectedIndex;
void setSelectIndex(int index) {
_selectedIndex = index;
update();
}
void searchLanguage(String query) {
if (query.isEmpty) {
_languages = [];
_languages = AppConstants.languages;
} else {
_selectedIndex = -1;
_languages = [];
AppConstants.languages.forEach((language) async {
if (language.languageName.toLowerCase().contains(query.toLowerCase())) {
_languages.add(language);
}
});
}
update();
}
}
In the LocalizationController we have a function
void loadCurrentLanguage() async {
_locale = Locale(sharedPreferences.getString(AppConstants.LANGUAGE_CODE) ??
AppConstants.languages[0].languageCode,
sharedPreferences.getString(AppConstants.COUNTRY_CODE) ??
AppConstants.languages[0].countryCode);
_isLtr = _locale.languageCode != 'bn';
for (int index = 0; index < AppConstants.languages.length; index++) {
if (AppConstants.languages[index].languageCode == _locale.languageCode) {
_selectedIndex = index;
break;
}
}
_languages = [];
_languages.addAll(AppConstants.languages);
update();
}
The above function, loads if we have any saved language in the app using locale(). Locale takes language code and country code.
We will also need a route file. let's define our routes.
class RouteHelper {
static const String initial = '/';
static const String splash = '/splash';
static const String language='/language';
static String getSplashRoute() => '$splash';
static String getInitialRoute()=>'$initial';
static String getLanguageRoute()=>'$language';
static List<GetPage> routes = [
GetPage(name: splash, page: () {
return SplashScreen();
}),
GetPage(name: initial, page:(){
return HomePage();
}),
GetPage(name:language, page:(){
return LanguagePage();
})
];
}
After splash screen user will go to LanguagePage(). On Languge screen user will be able to select a language and see the changes immediately.
Let's create our splash screen
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
@override
_SplashScreenState createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> with TickerProviderStateMixin{
late Animation<double> animation;
late AnimationController _controller;
GlobalKey<ScaffoldState> _globalKey = GlobalKey();
@override
dispose() {
_controller.dispose();
super.dispose();
}
void initState() {
super.initState();
_controller =
new AnimationController(vsync: this, duration: Duration(seconds: 2))..forward()
;
animation = new CurvedAnimation(parent: _controller,
curve: Curves.linear);
Timer(
Duration(seconds: 3),()=>Get.offNamed(RouteHelper.getLanguageRoute())
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _globalKey,
backgroundColor: Colors.white,
body: Column(
//crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
ScaleTransition(
scale: animation,
child: Center(child: Image.asset("img/logo part 1.png", width:200))),
Center(child: Image.asset("img/logo part 2.png", width:200,)),
],
),
);
}
}
After that we will create language screen. Here user will select the language and we will change the localization based on the user interaction.
class LanguagePage extends StatelessWidget {
final bool fromMenu;
LanguagePage({this.fromMenu = false});
@override
Widget build(BuildContext context) {
double? width = 375;
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: GetBuilder<LocalizationController>(builder: (localizationController) {
return Column(children: [
Expanded(child: Center(
child: Scrollbar(
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(5),
child: Center(child: SizedBox(
width: width,
child: Column(mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, children: [
Center(child: Image.asset("img/logo part 1.png", width: 120)),
SizedBox(height: 5),
Center(child: Image.asset("img/logo part 2.png", width: 140)),
SizedBox(height: 30),
Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: Text('select_language'.tr,),
),
SizedBox(height: 10),
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1,
),
itemCount: 2,//localizationController.languages.length,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) => LanguageWidget(
languageModel: localizationController.languages[index],
localizationController: localizationController, index: index,
)
),
SizedBox(height: 10),
Text('you_can_change_language'.tr, ),
]),
)),
),
),
)),
ElevatedButton(
child: Text('save'.tr),
onPressed: () {
if(localizationController.languages.length > 0 && localizationController.selectedIndex != -1) {
localizationController.setLanguage(Locale(
AppConstants.languages[localizationController.selectedIndex].languageCode,
AppConstants.languages[localizationController.selectedIndex].countryCode,
));
if (fromMenu) {
Navigator.pop(context);
} else {
Get.offNamed(RouteHelper.getInitialRoute());
}
}else {
Get.snackbar('select_a_language'.tr, 'select_a_language'.tr);
}
},
),
]);
}),
),
);
}
}
After selecting and saving the language we will go to home page
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Get.offAllNamed(RouteHelper.getLanguageRoute());
},
child: Text("home".tr)
),
)
);
}
}
Our language widget
class LanguageWidget extends StatelessWidget {
final LanguageModel languageModel;
final LocalizationController localizationController;
final int index;
LanguageWidget({required this.languageModel, required this.localizationController,
required this.index});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
localizationController.setLanguage(Locale(
AppConstants.languages[index].languageCode,
AppConstants.languages[index].countryCode,
));
localizationController.setSelectIndex(index);
},
child: Container(
padding: EdgeInsets.all(10),
margin: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(10),
boxShadow: [BoxShadow(
color: Colors.grey[200]!,
blurRadius: 5, spreadRadius: 1)],
),
child: Stack(children: [
Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
SizedBox(height: 5),
Text(languageModel.languageName, ),
]),
),
localizationController.selectedIndex == index ? Positioned(
top: 0, right: 0, left: 0, bottom: 40,
child: Icon(Icons.check_circle, color: Theme.of(context).primaryColor, size: 25),
) : SizedBox(),
]),
),
);
}
}
Message class is needed for internationalization
class Messages extends Translations {
final Map<String, Map<String, String>> languages;
Messages({required this.languages});
@override
Map<String, Map<String, String>> get keys {
return languages;
}
}
The above class extends Translations class of Getx
Now let's take a look at main.dart
Future<void> main() async {
// setPathUrlStrategy();
WidgetsFlutterBinding.ensureInitialized();
Map<String, Map<String, String>> _languages = await dep.init();
runApp(MyApp(languages: _languages));
}
class MyApp extends StatelessWidget {
final Map<String, Map<String, String>> languages;
MyApp({required this.languages });
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return GetBuilder<LocalizationController>(builder: (localizationController){
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primaryColor: Colors.blue,
fontFamily: "Lato",
),
locale: localizationController.locale,
translations: Messages(languages: languages),
fallbackLocale: Locale(AppConstants.languages[0].languageCode,
AppConstants.languages[0].countryCode),
initialRoute: RouteHelper.getSplashRoute(),
getPages: RouteHelper.routes,
defaultTransition: Transition.topLevel,
);
});
}
}
Here we have three properties local and translation and fallbacklocale. They are very important settings.
locale: We can set this property using locationzationController.locale. It's the default locale.
translations: This has to do with JSON file format. Here you have to mention your JSON key value pair type.
fallbacklocale: Here, we need to set up local(language) if default language is not found.