The application needs to implement language switching at runtime. Wrote a bloc with event and state and called BlocBuilder in main.dart. But I don't know how to implement the switch. How can I do that?
In total, the application has two languages.
My bloc:
class LanguageBloc extends Bloc<LanguageEvent, LanguageState> {
LanguageBloc() : super(InitialLang()) {
on<ChangeLang>(
(event, emit) {
emit(NewLang());
},
);
}
#immutable
abstract class LanguageEvent {}
class ChangeLang extends LanguageEvent {}
#immutable
abstract class LanguageState {}
class InitialLang extends LanguageState {}
class NewLang extends LanguageState {}
My main.dart
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => BottomNavyBloc(),
),
BlocProvider(
create: (context) => LanguageBloc(),
),
],
child: BlocBuilder<LanguageBloc, LanguageState>(
builder: (context, state) {
return MaterialApp(
title: 'Flutter Demo',
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: const HomeScreen(),
);
},
),
);
}
My lang btn:
ElevatedButton(onPressed: (){}, child: Text('Switch lang'))
What you can do is to send a variable Locale with the language of your choice, and in your MaterialApp in the locale attribute, you attach it.
Without complications you can use Cubit instead of Bloc, because it is not necessary to have events, then you could do the following:
class LanguageCubit extends Cubit<Locale?> { // change state here, you dont use LanguageState
LanguageCubit() : super(null);
void initialLang () { // your initial lang
emit(
Locale("en", ""),
);
}
void newLang(
bool isEnglish, // in your checkbox you are gonna send the boolean value here
) {
emit(
isEnglish ? Locale("en") : Locale("fr"),
);
}
}
Now in your main, as you have it, it would only look like this:
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => BottomNavyBloc(),
),
BlocProvider(
create: (context) => LanguageBloc(),
),
],
child: BlocBuilder<LanguageBloc, Locale?>( // change the state for Locale? cause could be null
builder: (context, lang) { // different name to lang
return MaterialApp(
title: 'Flutter Demo',
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
theme: ThemeData(
primarySwatch: Colors.blue,
),
locale: lang, // here you set the lang
debugShowCheckedModeBanner: false,
home: const HomeScreen(),
);
},
),
);
}
----- EDIT WITH BUTTON -----
I thought you would need a Switch Button that handles the booleans but no, you only need one button that will be the one to change it so I did it this way:
class _HomePageState extends State<HomePage> {
bool _currentLanguageBool = false; // variable to know if is english or french
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => LanguageCubit(), // with this we can use the Cubit in all the page, normally you have to have it in main and in the MaterialApp
child: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
BlocProvider.of<LanguageCubit>(context)
.newLang(_currentLanguageBool);
setState(() {
_currentLanguageBool = !_currentLanguageBool;
}); // with this you change the variable
},
child: Text('Switch lang'),
),
),
),
);
}
}
We will make our widget a StatefulWidget so we can just change the boolean variable and know if it is in English or French. If you don't want to use Stateful let me know, because we can use it with the same Cubit, but it would change the code and a little bit the logic of the LanguageCubit.
Related
I am developing an app in Flutter. I'm using flutter_localizations package for localization and intl package for internationalization. For this, I'm using Context in Widgets, but the problem is when I want to use internationalization inside bloc or repositories or other layers except for the UI layer.
What is the best practice for doing internationalization inside Other layers except for UI where we don't have access to Context?
I have tried to use a Singleton, but I don't know if this is the right way.
You have to pass appLocalizations. For example in the Cubits you can do something like:
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();
}
}),
),
);
In your bloc or cubit:
class UserAuthCubit extends Cubit<UserAuthState> {
final UserAuthRepository repository;
final AppLocalizations localizations;
UserAuthCubit({
#required this.repository,
#required this.localizations,
}) : super(const UserAuthInitial()) {
getUserAuthState();
}
[PLEASE SEE EDIT BELOW]
I am using named routing (with arguments) to send a user from page2 back to page1 in my app when a button is pressed:
onPressed: () {
bool resumeProcess = true;
Navigator.pushNamed(context, '/',
arguments: RouteArguments(resumeProcess: resumeProcess));
},
My named routing is set up in my MaterialApp:
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
final RouteArguments args =
ModalRoute.of(context)!.settings.arguments as RouteArguments;
return MaterialApp(
title: 'Reactive BLE Test',
theme: ThemeData(
primarySwatch: Colors.pink,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/',
routes: {
'/': (context) => MyHomePage(
title: 'Reactive BLE Test',
resumeProcess: args.resumeProcess,
),
'/myWifiPage': (context) => const MyWifiPage(),
},
navigatorKey: navigatorKey,
);
}
}
I'm using the following class in conjunction with ModalRoute to retrieve args.resumeProcess:
class RouteArguments {
final bool resumeProcess;
RouteArguments({required this.resumeProcess});
}
and
final RouteArguments args =
ModalRoute.of(context)!.settings.arguments as RouteArguments;
I'm unable to retrieve args.resumeProcess because my bang operator is being used on a null value at runtime.
I have tried different ways to solve this problem with no success. There are some good posts about this, but none seem to fit my use case (or maybe I'm missing something).
Is there a better way to send these arguments, or is there a way to deal with this null problem at runtime?
[BEGIN EDIT HERE]
While trying to fix the problem above, I turned to the recipe for doing this provided in the Flutter Docs: Pass arguments to a named route. This resulted in the following changes.
New ScreenArguments Class:
class ScreenArguments {
final String title;
final String resumeProcess;
ScreenArguments({required this.title, required this.resumeProcess});
}
New ExtractArgumentsScreen Class
class ExtractArgumentsScreen extends StatelessWidget {
const ExtractArgumentsScreen({super.key});
static const routeName = '/';
#override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;
return Scaffold(
appBar: AppBar(
title: Text(args.title),
),
body: Center(
child: Text(args.resumeProcess),
),
);
}
}
My MaterialApp is located in MyApp. This is where I register the widget in the routes table per the docs. I've commented out the old routes.
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Reactive BLE Test',
theme: ThemeData(
primarySwatch: Colors.pink,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/',
// routes: {
// '/': (context) => MyHomePage(
// title: 'Reactive BLE Test',
// resumeProcess: args.resumeProcess,
// ),
// '/myWifiPage': (context) => const MyWifiPage(),
// },
routes: {
ExtractArgumentsScreen.routeName: (context) =>
const ExtractArgumentsScreen(),
},
navigatorKey: navigatorKey,
);
}
}
And finally, I navigate to the widget in my second page (returning from page 2 to page 1) using Navigator.pushNamed().
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: const Text('Reactive BLE Test'),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
String title = 'Reactive BLE Test';
String resumeProcess = 'true';
Navigator.pushNamed(
context,
ExtractArgumentsScreen.routeName,
arguments: ScreenArguments(
title: title,
resumeProcess: resumeProcess,
),
);
},
),
actions: const [],
),
body: Column(
children: <Widget>[
I get a new, but similar error:
type 'Null' is not a subtype of type 'ScreenArguments' in type cast
Can anyone tell me what am I doing wrong? Thanks in advance.
I think you are calling it on the wrong place. these ModalRoute.of(context) must be associated with context that having modal routes, or routes. in this case MaterialApp which hold the routes. But you are calling it before the MaterialApp is created. try to move it into inside of MyHomePage and read the arguments inside there instead of passing it as arguments on the material app Routes.
for example:
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Reactive BLE Test',
theme: ThemeData(
primarySwatch: Colors.pink,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/',
routes: {
'/': (context) => MyHomePage(
title: 'Reactive BLE Test',
),
'/myWifiPage': (context) => const MyWifiPage(),
},
navigatorKey: navigatorKey,
);
}
and inside your MyHomePage widget:
#override
Widget build(BuildContext context) {
final RouteArguments args =
ModalRoute.of(context)!.settings.arguments as RouteArguments;
var resumeProcess = args.resumeProcess;
return Scaffold(
// your home page class
);
}
Bloc is good because it works without context. Internalization requires context. You can try to pass it to the block, but in my case, the bloc is initialized before the internalization. MultiProvider before MaterialApp, how to change the sequence and use internalization inside bloc?
class MyApp extends StatelessWidget {
final routes = <String, WidgetBuilder>{
StartPage.routeName: (BuildContext context) => new StartPage(),
EditorPage.routeName: (BuildContext context) => new EditorPage(),
};
#override
Widget build(BuildContext context) {
//localization always == null
var localization = S.of(context);
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: ChannelModel(),
),
BlocProvider<InternetBloc>(
create: (context) => InternetBloc(localizations: localization),
),
BlocProvider<MultiBloc>(
create: (context) => MultiBloc(),
),
BlocProvider<DbBloc>(
create: (context) => DbBloc(),
),
BlocProvider<FileBloc>(
create: (context) => FileBloc(),
),
],
child: MaterialApp(
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: S.delegate.supportedLocales,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: StartPage(),
routes: routes,
));
}
}
I put provider above material app so I can use it in every widget in-app right?
so why this error
and my code is
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Cart(),
),
ChangeNotifierProvider.value(value: ProductsProvider()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.purple,
accentColor: Colors.deepOrange,
fontFamily: 'Lato',
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ProductOverviewScreen(),
routes: {ProductDetailScreen.routeName: (ctx) => ProductDetailScreen()},
),
);
}
}
and this screen has the error
enum filterOptions { Favorites, All }
class ProductOverviewScreen extends StatefulWidget {
#override
_ProductOverviewScreenState createState() => _ProductOverviewScreenState();
}
class _ProductOverviewScreenState extends State<ProductOverviewScreen> {
var _showOnlyFavorites = false;
#override
Widget build(BuildContext context) {
//final cart = Provider.of<Cart>(context);
return Scaffold(
appBar: AppBar(
title: Text("MyShop"),
actions: [
PopupMenuButton(
onSelected: (selectedValue) {
setState(() {
if (selectedValue == filterOptions.Favorites) {
_showOnlyFavorites = true;
} else if (selectedValue == filterOptions.All) {
_showOnlyFavorites = false;
}
});
},
icon: Icon(Icons.more_vert),
itemBuilder: (_) => [
PopupMenuItem(
child: Text("Only Favorites"),
value: filterOptions.Favorites),
PopupMenuItem(
child: Text("Show All"), value: filterOptions.All),
]),
Consumer<Cart>(
builder: (_, cartData, ch) => Badge(
child: ch,
value: cartData.itemCount.toString(),
),
child: IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {},
),
)
],
),
body: ProductsGrid(_showOnlyFavorites));
}
}
the error in the consumer is
Error: Could not find the correct Provider above this Consumer Widget
why does this screen cant know the Cart provider?
any help please ?
I don't have enough reputation to comment, but your sample works fine on my end with flutter 1.22.5 and provider 4.3.2. However, I managed to reproduce your problem when accidentally importing a package named flutter_provider and using its Consumer widget. Couldn't imagine this being your problem though.
By the way, you should avoid using the value constructor to create your ChangeNotifier. Either pass a variable or use the default constructor with the create parameter.
localizaed.dart
import 'package:flutter/cupertino.dart';
class Localized {
Localized(this.locale);
final Locale locale;
static Localized of(BuildContext context) => Localizations.of<Localized>(context, Localized);
static Map<String, Map<String,String>> _v = {
'en': {
'title': 'hello world',
},
'ja': {
'title': 'こんちは'
}
};
String get title => _v[locale.languageCode]['title'];
}
class LocalizedDelagate extends LocalizationsDelegate<Localized> {
const LocalizedDelagate();
#override
bool isSupported(Locale locale) => ['en','ja'].contains(locale.languageCode);
#override
Future<Localized> load(Locale locale) async => Localized(locale);
#override
bool shouldReload(LocalizationsDelegate old) => false;
}
main.dart
import 'package:calculator/src/localization/localized.dart';
import 'package:flutter/material.dart';
import 'package:calculator/src/pages/HomePage.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
void main() => runApp(StockCalcApp());
class StockCalcApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
const LocalizedDelagate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [
const Locale('en'),
const Locale('ja'),
],
onGenerateTitle: (BuildContext context) => Localized.of(context).title, // notthing problem
theme: ThemeData(primarySwatch: Colors.pink),
home: Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text(Localized.of(context).title), // and same code but, error. when I comment this line then, nothing well.
backgroundColor: Colors.pink[900],
elevation: 0.0,
),
body: StockHome(),
),
);
}
}
I cannot understand why to occur an error this message on the screen.
NoSuchMethodError: The getter 'title' was called on null.
I just do this example find on the web. and I think.. is very simple .. I think.
but seriously I cannot understand why to bring up this message on the android emulator.
When you call onGenerateTitle: (BuildContext context) => Localized.of(context).title, it uses a new BuildContext, which already contains the LocalizedDelagate(), so it can be called with Localized.of(context).
When you use it within the same build method, you refer to an instance of context before the LocalizedDelagate() was created, so Localized.of(context) doesn't return anything.
You can avoid this problem by creating a new Widget, which will have an updated BuildContext in it's build method, that will have access to Localized.
For example, create a new widget named HomeScreen
class StockCalcApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
const LocalizedDelagate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
supportedLocales: [
const Locale('en'),
const Locale('ja'),
],
onGenerateTitle: (BuildContext context) => Localized.of(context).title,
theme: ThemeData(primarySwatch: Colors.pink),
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text(Localized.of(context).title), // this context will have access to Localized
backgroundColor: Colors.pink[900],
elevation: 0.0,
),
body: StockHome(),
);
}
}