Flutter - app localization using intl translations for messages inside Cubit/ Bloc - flutter

I am working on a multilingual app in Flutter.
I had no problem with implementing the localized strings in widgets/ screens following the official docs: https://flutter.dev/docs/development/accessibility-and-localization/internationalization.
But ... my app does a lot of calls to an API which are handled behind the scenes by appropriate Cubits and Repositories. For these calls and other deeper logic I would like to provide status messages in appropriate languages (e.g. for snackbars). The problem I face is that I cannot access localized strings inside the Cubits to provide messages to the state. Even if I try to pass the context to the Cubit it does not see them.
Anyone have an idea please? I would prefer following the official approach and not having to totally refactor the app ...
Thanks in anticipation!

Somebody helped me with this.
The solution is simple(ish).
You have to pass appLocalizations themselves into the Cubits. Here is my main.dart:
runApp(
MaterialApp(
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''),
const Locale('pl', ''),
],
title: 'MySuperApp',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.orange,
accentColor: Colors.deepOrangeAccent,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/',
routes: {
UserAuthScreen.routeName: (context) => const UserAuthScreen(),
HomePage.routeName: (context) => HomePage(),
...
},
builder: (context, child) {
final appLocalizations = AppLocalizations.of(context); //IMPORTANT
return MultiBlocProvider(
providers: [
BlocProvider<ConstantsCubit>(
lazy: true,
create: (context) => ConstantsCubit(
constantsRepository: ConstantsRepository(),
),
),
BlocProvider<UserAuthCubit>(
lazy: true,
create: (context) => UserAuthCubit(
localizations: appLocalizations, //THIS IS WHERE THE MAGIC HAPPENS
repository: UserAuthRepository(),
),
),
BlocProvider<DoerInfoCubit>(
lazy: true,
create: (context) => DoerInfoCubit(
doerInfoRepository: DoerInfoRepository(),
userAuthCubit: BlocProvider.of<UserAuthCubit>(context),
)),
...
],
child: child,
);
},
home:
BlocBuilder<UserAuthCubit, UserAuthState>(builder: (context, state) {
if (state is UserAuthLogged) {
return HomePage();
} else {
return const UserAuthScreen();
}
}),
),
);
The UserAuthCubit declaration looks like this:
class UserAuthCubit extends Cubit<UserAuthState> {
final UserAuthRepository repository;
final AppLocalizations localizations; //THIS IS WHERE THE MAGIC HAPPENS
UserAuthCubit({
#required this.repository,
#required this.localizations,
}) : super(const UserAuthInitial()) {
getUserAuthState();
}
IMPORTANT - to make sure the solutions works - AppLocalizatons have to be declared before the Cubits in the main.dart.
ALSO - the app adopts new localizations in the widgets straight away after language change. However with this setup I was not able to achieve this for Cubits. They need a restart of the app. In real life scenario this should not be a problem ... I guess.

It's easy to do with the easy_localization package. The only downside is code generation to be done on every string change. Otherwise it doesn't need a context to get a string, like so:
LocaleKeys.app_title.tr()
Check out the whole tutorial on how to setup and use EasyLocalization.

Related

Pages stack can be managed by either the Widget (AutoRouter.declarative) or the (StackRouter)

I am using Declarative Routing from AutoRoute flutter package.
class App extends StatelessWidget {
final _appRouter = AppRouter();
#override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: AutoRouterDelegate.declarative(
_appRouter,
routes: (_) => [
// if the user is logged in, they may proceed to the main App
if (authService().isLoggedIn)
HomeRoute()
// if they are not logged in, bring them to the Login page
else
LoginWrapperRoute(onLogin: () => authService().logIn),
],
),
routeInformationParser:
_appRouter.defaultRouteParser(includePrefixMatches: true));
}
}
IconButton(
onPressed: () {
context.pushRoute(const AlarmRoute());
},
icon: const Icon(Icons.notifications_active),
color: const Color(0xFF666666),
)
when I try to push or navigate to a page it gives me this error:
Widget': Pages stack can be managed by either the Widget (AutoRouter.declarative) or the (StackRouter)
Problem
As per the author of auto_route on their comment:
You can't use context.pushRoute (imperative) if you use the
declarative navigation, AutoRouterDelegate.declarative. . You have
to pick one.
There are many issues open so it would be great if auto_route can implement an improvement.
My solution:
I avoid using the "declarative" approach by auto_route, but I still have similar code. Near the root of my app, I had:
return AnimatedBuilder(
animation: settingsController,
builder: (BuildContext context, Widget? child) {
return MaterialApp.router(
restorationScopeId: 'app',
routerDelegate: _appRouter.delegate(
initialRoutes: (settingsController.settings.showOnboarding)
? [const OnboardingRoute()]
: [CurrentPoseRoute()]),
routeInformationParser: _appRouter.defaultRouteParser(),
theme: ThemeData(),
darkTheme: ThemeData.dark(),
themeMode: settingsService.settings.themeMode.toMaterial(),
);
},
);
My settingsController uses the mixin ChangeNotifier, so I could use notifyListeners();.
This does build a big piece of widget tree though. For me, it doesn't matter because I use this when switching from onboarding to normal user flow.

How do you use the new Flutter ScreenUtils with named Routes and Provider?

Right until version 5.1.0 (which were the ones we used) the package flutter_screenutil were using a simple concept of builder which asked a "Widget Function() Builder". Which worked well on how we did it.Like so:
return ScreenUtilInit(
designSize: const Size(360, 760),
minTextAdapt: true,
splitScreenMode: true,
builder: () => MultiProvider(
providers: [
...
],
child:Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return MaterialApp(
...
initialRoute: '/',
routes: {
'/': (context) => devMode? Home(): DevHome()
However, new iterations of flutter_screenutil (now on 5.5) have been asking for a builder which is a"Widget function(child) Builder"
And they suggest this:
//Set the fit size (Find your UI design, look at the dimensions of the device screen and fill it in,unit in dp)
return ScreenUtilInit(
designSize: const Size(360, 690),
minTextAdapt: true,
splitScreenMode: true,
builder: (child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'First Method',
// You can use the library anywhere in the app even in theme
theme: ThemeData(
primarySwatch: Colors.blue,
textTheme: Typography.englishLike2018.apply(fontSizeFactor: 1.sp),
),
home: child,
);
},
child: const HomePage(title: 'First Method'),
);
}
But they don't give instructions to make it work as intended, either on the documentation, examples, or how to use it on how to deal with both named routes or state management things like the provider, and when I tried this:
return ScreenUtilInit(
designSize: const Size(360, 760),
minTextAdapt: true,
splitScreenMode: true,
child: devMode? Home(): DevHome()
builder: (child) => MultiProvider(
providers: [
...
],
child:Consumer<SettingsProvider>(
builder: (context, settingsProvider, child) {
return MaterialApp(
...
initialRoute: '/',
routes: {
'/': (context) => child!
It just makes a restart of the app every time I change orientation.
I am not sure what I am doing wrong and the documentation isn't making things easier to understand either. As it does not explain how to use this Builder and child, and the code does not make it clear for me how to use it either.
Can someone help me understand this conundrum? We are stuck in ancient version with some serious bugs which should be fixed, But we cant upgrade cause I cant make flutter_screenutil work as intended.
Thank you.

ChangeNotifierProxyProvider getting null value

I am new to flutter.
In my application locale information is found when the user login.
So the idea is when the user login, it will pass the locale to AppLanguage.
I have written ChangeNotifierProxyProvider to get the locale inside authentication information and create a AppLanuage object
In the ChangeNotifierProxyProvider I am getting appLang as null. auth object is correctly NOT null.
What I don't understand why I am getting null?
I did create it here right?
create: (_) => AppLanguage(),
shouldn't it come as a parameter for the update?
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: Auth()),
ChangeNotifierProxyProvider<Auth, AppLanguage>(
create: (_) => AppLanguage(),
update: (ctx, auth, appLang) {
print(auth);
print(appLang);
}
//appLang.setLocale(auth == null ? 'en' : auth.language),
),
],
child: Consumer2<Auth, AppLanguage>(
builder: (ctx, auth, lang, child) => MaterialApp(
title: 'Test App',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
locale: lang.appLocal,
supportedLocales: [
const Locale('en', 'US'),
const Locale('ja', ''),
],
localizationsDelegates: [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
home: LandingView(),
),
),
);
}
I would try something like:
ChangeNotifierProxyProvider<Auth, AppLanguage>(
create: (_) => AppLanguage(),
update: (ctx, auth, appLang) => appLang..update(auth),
),
class AppLanguage with ChangeNotifier {
void update(Auth auth) {
// Do some custom work based on myModel that may call `notifyListeners`
}
}
That way your child will be able to get the correct updated values when they're available.
You can check more how to deal properly with that on the provider docs.

Why does flutter localization not work properly?

I want to add the tr tag to the Flutter localization section. I am
getting an error although there is support for Tr. The error is as
follows; error: The element type 'Locale (where Locale is defined in
C:\Users\StatTark\AppData\Roaming\Pub\Cache\hosted\pub.dartlang.org\intl-0.16.1\lib\src\locale.dart)'
can't be assigned to the list type 'Locale (where Locale is defined in
C:\flutter\bin\cache\pkg\sky_engine\lib\ui\window.dart)'.
error: Abstract classes can't be instantiated.
(instantiate_abstract_class at [ajanda] lib\pages\mainmenu.dart:36)
I do not understand if I am making a mistake in use, I will be glad if you help.
class MainMenu extends StatelessWidget {
MainMenu({Key key}) : super(key: key);
final _sdb = SettingsDbHelper();
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _sdb.getSettings(),
builder: (context, snapshot) {
if (snapshot.data == null) {
return MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [Locale('en','US'),Locale('tr','')], //The error is here
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Text(proTranslate["Yükleniyor....."][Language.languageIndex]),
),
),
);
} else {
Language.languageIndex = snapshot.data[0].language;
return DynamicTheme(
defaultBrightness: Brightness.light,
data: (brightness) => ThemeData(
brightness: brightness,
fontFamily: snapshot.data[0].fontName,
floatingActionButtonTheme: FloatingActionButtonThemeData(
foregroundColor: Colors.green,
),
),
themedWidgetBuilder: (context, theme) {
return MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [Locale('en','US'),Locale('tr','')],
debugShowCheckedModeBanner: false,
theme: theme,
home: MainMenuBody(
warning: snapshot.data[0].warning,
),
// navigatorKey: navigatorKey,
);
});
}
},
);
}
}
class MainMenuBody extends StatefulWidget {....}
I'm coming a bit late but I just fixed this same issue I had.
I'm pretty sure your file is importing intl/locale.dart instead of flutter/material.dart as both define a Local type.
To fix it, just replace your import at the top of the file from:
import 'package:intl/locale.dart';
to
import 'package:flutter/material.dart';
and you should be OK.
Few suggestions you can try/test, see below.
Replace [Locale('en','US'),Locale('tr','')], with [Locale('en'),Locale('tr')],
init list of supported locales first and use it accordingly.
// init before build
final List<Locale> appSupportedLocales = [
Locale('en'),
Locale('tr')
];
// ...
// then use it like this
supportedLocales: appSupportedLocales,

Could not find the correct Provider<HomeBloc> above this RestoreLocalBackupPage Widget, how to solve this issue in a simpler manner than what I did?

I'm trying to build a Notes app with backup and restore functionality. I have a home page that shows up when the app is opened. This page has a Scaffold as it's body, which in turn has a drawer that has ListTiles for backup and restore. I use a HomeBloc object to interact with the database where I save the notes, hence I used Provider to get access to it everywhere.
The ListTiles open a MaterialPageRoute to new screens where the user is prompted to choose the file, enter passwords etc.
When I tap on the Restore ListTile in the drawer, I get this error:
The following ProviderNotFoundException was thrown building RestoreLocalBackupPage(dirty, state: _RestoreLocalBackupPageState#4f937):
Error: Could not find the correct Provider<HomeBloc> above this RestoreLocalBackupPage Widget
This likely happens because you used a `BuildContext` that does not include the provider
of your choice.
This is my main.dart, where I wrap the Home page in a Provider:
void main() {
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Notes',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Provider(
create: (_) => HomeBloc(),
child: HomePage(),
)
);
}
}
This is the build method of my HomePage:
Widget build(BuildContext context) {
homeBloc = Provider.of<HomeBloc>(context);
return Scaffold(
backgroundColor: Color.fromRGBO(219, 243, 250, 1),
appBar: AppBar(...),
body: StreamBuilder<List<Note>>(...),
floatingActionButton: FloatingActionButton(...),
drawer: HomeDrawer(),
);
}
The HomeDrawer's build method returns a Drawer, which has a ListView as it's child. Here's the code for the ListTile that launches the Restore Backup page:
ListTile(
title: Text('Local Backup',
style: GoogleFonts.sourceSansPro(
textStyle: TextStyle(fontWeight: FontWeight.w500),
fontSize: 16)),
onTap: () async {
// Update the state of the app
// ...
// Then close the drawer
bool permissionGranted = await _getPermissions(context);
if (permissionGranted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CreateLocalBackupPage(
currentBackupDirectory: currentBackupDirectory
),
)
);
}
},
)
This is the error that I get when I tap on the above ListTile:
The following ProviderNotFoundException was thrown building RestoreLocalBackupPage(dirty, state: _RestoreLocalBackupPageState#4f937):
Error: Could not find the correct Provider<HomeBloc> above this RestoreLocalBackupPage Widget
This likely happens because you used a `BuildContext` that does not include the provider
of your choice.
HomeDrawer()'s BuildContext does have access to the HomeBloc object I need. Hence, wrapping the RestoreLocalBackupPage widget inside another Provider works:
HomeBloc homebloc = Provider.of<HomeBloc>(context);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Provider(
create: (_) => homebloc,
child: RestoreLocalBackupPage(currentBackupDirectory: currentBackupDirectory),
)
)
);
I wanted to know if there's a simpler, more elegant way of getting access to HomeBloc inside RestoreLocalBackupPage using Provider. Dependency Injection via the constructor works but that sort of defeats the purpose of using Provider in the first place, right?
Wrapping the MaterialApp in main.dart with a Provider solved my issue. I have found the solution here. Check rrousselGit's answer.
After doing that, main.dart now becomes:
void main() {
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
LicenseRegistry.addLicense(() async* {
final license = await rootBundle.loadString('google_fonts/Cabin_OFL.txt');
yield LicenseEntryWithLineBreaks(['google_fonts_Cabin'], license);
});
LicenseRegistry.addLicense(() async* {
final license = await rootBundle.loadString('google_fonts/SSP_OFL.txt');
yield LicenseEntryWithLineBreaks(['google_fonts_SSP'], license);
});
return Provider(
create: (_) => HomeBloc(),
child: MaterialApp(
title: 'Notes',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
),
);
}
}
Try to use provider one level up and wrap MaterialApp with Provider.