I find the localization procedure using the official Flutter localization plugin cumbersome. To display a localized string I have to call AppLocalizations.of(context).myAppTitle - not exactly sleek or easy to glance over in a huge nested Widget tree with lots of localized strings. Not to mention it looks ugly.
Is there a way to make the usage nicer? For example, can I use a global variable or a static class with a AppLocalizations instance member to make the access easier? For example declaring a top level AppLocalizations variable
// Somewhere in the global scope
AppLocalizations l;
// main.dart
class _MyAppState extends State<MyApp>{
#override
void initState() {
super.initState();
getLocaleSomehow().then((locale){
l = Localization(locale);
setState((){});
});
}
}
Then I could simply call
Text(l.myAppTitle)
So in an essence what I'm asking is "what are the dangers and/or disadvantages of not calling AppLocalizations.of(context)?"
If I really do need to use the .of(BuildContext) method to access the AppLocalizations instance - can I at least store it in my StatefulWidget? I'm thinking something like
class DetailsPage extends StatefulWidget{
AppLocalizations _l;
#override
Widget build(BuildContext context) {
_l = AppLocalizations.of(context);
// ... build widgets ...
}
}
Or is there any other way to make the localization less cumbersome?
Yes, it is needed.
You could work around it, but that is a bad idea.
The reason for this is that Localization.of<T>(context, T) may update over time. A few situations where it does are:
The locale changed
The LocalizationsDelegate obtained was asynchronously loaded
MaterialApp/CupertinoApp got updated with new translations
If you're not properly calling Localization.of inside build as you should, then in those scenarios your UI may fail to properly update.
It is totally fine to store the Localization object inside of your State and it works very well in that case.
If you want to only make it look nicer, you could also just declare the variable in the build method:
#override
Widget build(BuildContext context) {
final l = Localization.of(context);
return Text(l.myAppTitle);
}
In a StatefulWidget, you could also re-assign the variable in didChangeDependencies or just assign it once using the null-aware ??= operator because the object will not change over time:
class _MyStatefulWidgetState extends State<MyStatefulWidget> with WidgetsBindingObserver {
Localization l;
#override
didChangeDependencies() {
WidgetsBinding.instance.addObserver(this);
l ??= Localization.of(context);
super.didChangeDependencies();
}
#override
void didChangeLocales(List<Locale> locale) {
l = Localization.of(context);
super.didChangeLocales(locale);
}
#override
dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) => Text(l.myAppTite);
}
In didChangeLocales, you can re-assign every time. This makes sure that the variable always holds the appropriate locale and is initialized at first build (with didChangeDependencies). Notice that I also included a WidgetsBindingObserver, which you need to handle as shown in the code.
By using Flutter extensions you can now simply extend The StatelessWidget and StatefulWidget, or the generic Widget to provide a translate method. Two different solutions:
1. on Widget
extension TranslationHelper on Widget {
String tr(BuildContext context, String key) {
return AppLocalizations.of(context).translate(key);
}
}
Then in the build method of a StatelessWidget you can call tr(context, 'title'). For the build method of a StatefulWidget you have to call widget.tr(context, 'title').
2. on StatelessWidget and StatefulWidget
For a more consistent calling of the translate function you can extend StatelessWidget and StatefulWidget, respectively:
extension TranslationHelperStateless on StatelessWidget {
String tr(BuildContext context, String key) {
return AppLocalizations.of(context).translate(key);
}
}
extension TranslationHelperStateful<T extends StatefulWidget> on State<T> {
String tr(BuildContext context, String key) {
return AppLocalizations.of(context).translate(key);
}
}
For both build methods in StatelessWidget and StatefulWidget you can call:
tr(context, 'title')
With StatefulWidget there is one risk as a developer you need to avoid. Which is calling the tr() method in a place where you can access context but where the build method has not ran yet, like initState. Make sure to call tr() always in the build methods.
3. on StatelessWidget and StatefulWidget, but not using the translate method
You can extend StatelessWidget and StatefulWidget and return the AppLocalizations, like this:
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension TranslationHelperStateless on StatelessWidget {
AppLocalizations tr(BuildContext context) {
return AppLocalizations.of(context)!;
}
}
extension TranslationHelperStateful<T extends StatefulWidget> on State<T> {
AppLocalizations tr(BuildContext context) {
return AppLocalizations.of(context)!;
}
}
For both build methods in StatelessWidget and StatefulWidget you can call:
tr(context).title
or
tr(context).helloUser(name)
I combined some of the info from the other responses here (specialy Fleximex's) to a solution which I found quite interesting - and that's the one I'm using. I created an extension on the BuildContext itself.
extension BuildContextHelper on BuildContext {
AppLocalizations get l {
// if no locale was found, returns a default
return AppLocalizations.of(this) ?? AppLocalizationsEn();
}
}
Using this extension (and importing it), one can use it like this:
context.l.appTitle
or
context.l.helloUser(name)
IMHO it's clean and readable, it's not the shortest though.
You can create your own text widget and do localization there.You can replace all your text widgets with your own MyText widget.
class MyText extends StatelessWidget {
String data;
InlineSpan textSpan;
TextStyle style;
StrutStyle strutStyle;
TextAlign textAlign;
TextDirection textDirection;
Locale locale;
bool softWrap;
TextOverflow overflow;
double textScaleFactor;
int maxLines;
String semanticsLabel;
TextWidthBasis textWidthBasis;
MyText(
this.data, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
});
#override
Widget build(BuildContext context) {
return Text(
Localization.of(context).data,
style: style,
semanticsLabel: semanticsLabel,
locale: locale,
key: key,
textAlign: textAlign,
maxLines: maxLines,
overflow: overflow,
softWrap: softWrap,
strutStyle: strutStyle,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
textWidthBasis: textWidthBasis,
);
}
}
Related
What is the rule of thumb to use an initial method for a widget. Shall I use the:
A. classical stateful widget approach?
Or is it better to stick with the B. stateless widget approach?
Both seem to work from my testing. In terms of code reduction, it seems the B. approach is better, shorter, cleaner, and more readable. How about the performance aspect? Anything else that I could be missing?
Initializing a controller should be a one-time operation; if you do it on a StatelessWidget's build method, it will be triggered every time this widget is rebuilt. If you do it on a StatefulWidget's initState, it will only be called once, when this object is inserted into the tree when the State is initialized.
I was looking for initializing some values based on values passed in constructor in Stateless Widget.
Because we all know for StatefulWidget we have initState() overridden callback to initialize certain values etc. But for Stateless Widget no option is given by default. If we do in build method, it will be called every time as the view update. So I am doing the below code. It works. Hope it will help someone.
import 'package:flutter/material.dart';
class Sample extends StatelessWidget {
final int number1;
final int number2;
factory Sample(int passNumber1, int passNumber2, Key key) {
int changeNumber2 = passNumber2 *
2; //any modification you need can be done, or else pass it as it is.
return Sample._(passNumber1, changeNumber2, key);
}
const Sample._(this.number1, this.number2, Key key) : super(key: key);
#override
Widget build(BuildContext context) {
return Text((number1 + number2).toString());
}
}
Everything either a function or something else in widget build will run whenever you do a hot reload or a page refreshes but with initState it will run once on start of the app or when you restart the app in your IDE for example in StatefulWidget widget you can use:
void initState() {
super.initState();
WidgetsBinding.instance!
.addPostFrameCallback((_) => your_function(context));
}
To use stateful functionalities such as initState(), dispose() you can use following code which will give you that freedom :)
class StatefulWrapper extends StatefulWidget {
final Function onInit;
final Function onDespose;
final Widget child;
const StatefulWrapper(
{super.key,
required this.onInit,
required this.onDespose,
required this.child});
#override
State<StatefulWrapper> createState() => _StatefulWrapperState();
}
class _StatefulWrapperState extends State<StatefulWrapper> {
#override
void initState() {
// ignore: unnecessary_null_comparison
if (widget.onInit != null) {
widget.onInit();
}
super.initState();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
#override
void dispose() {
if (widget.onDespose != null) {
widget.onDespose();
}
super.dispose();
}
}
Using above code you can make Stateful Wrapper which contains stateful widget's method.
To use Stateful Wrapper in our widget tree you can just wrap your widget with Stateful Wrapper and provide the methods or action you want to perform on init and on dispose.
Code available on Github
NOTE: You can always add or remove method from Stateful Wrapper Class according to your need!!
Happy Fluttering!!
I recently tried to create an abstract widget, that has then both a stateless and a stateful implementation, which both can be accessed via factory-methods.
Below I added a minimal example of the only real working solution I have figured out that works for my use case, but it leaves me with some things to be desired.
For example with this solution, I have to declare and override every variable in the sub-classes, while I would really like to rely on the fact that they are subtypes and implicitly have those variables.
Has anyone of you ever needed to do a similar thing? Have you worked out a different approach?
For those concerned about as to why I would need this: I wanted to make a singular Button-Class for my App, that then has different implementations for specific styles of buttons (regular button, a 'striped' button, a button that 'loads' as the user scrolls down a page and becomes active once the user reached the end of the page, etc.). That way I could then simply call 'Button.implementation' wherever i needed a specific button, and have all the button-related Code in the same place.
Cheers.
abstract class A {
final int intellect;
A(this.intellect);
factory A.giveMeB(int intellect) {
return _B(intellect);
}
factory A.giveMeC(int intellect) {
return _C(intellect);
}
}
class _B extends StatelessWidget implements A {
#override
final int intellect;
_B(this.intellect);
#override
Widget build(BuildContext context) {
return SizedBox.shrink();
}
}
class _C extends StatefulWidget implements A {
#override
final int intellect;
const _C(this.intellect, {Key key}) : super(key: key);
#override
_CState createState() => _CState();
}
class _CState extends State<_C> {
#override
Widget build(BuildContext context) {
return Container();
}
}
To start, I'm new to Flutter, so I am completely open to the possibility that my problem stems from a fundamental misunderstanding, but here is my question:
I am trying to get a good understanding of how to use Provider in conjunction with with the get_it package.
I think I understand how to use the Provider pattern in the standard case, by which I mean creating a unique class with a view and a view_model. Where I seem to have become lost is when I design a custom widget as a base template class and then extend that widget so that it can be tailored for use in a specific class view, I'm not seeing how to connect it to the Provider pattern because the base class doesn't know in advance which view_model it needs to listen to.
Below I will provide short example of what I am doing in the standard case, where things seem to work fine, and then I will show a short example of how I am trying to build the custom widget and extend it...
Here is the sample standard way in which I am using the Provider pattern with get_it, in which everything seems to work just fine:
class MyScreenView extends StatefulWidget{
#override
_ProfileEditScreenViewState createState() => _ProfileEditScreenViewState();
}
class _MyScreenViewState extends State<MyScreenView>{
final MyScreenViewModel model = serviceLocator<MyScreenViewModel>();
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<MyScreenViewModel>(
create: (context) => model,
child: Material(
color: Colors.white,
child: Consumer<MyScreenViewModel>(
builder: (context,model,child) => Text(model.someText),
),
),
);
}
}
class MyScreenViewModel extends ChangeNotifier{
String? _someText;
MyScreenViewModel() {
this._someText= 'Sample Text';
}
String get someText=> _someText;
set someText(String value) {
_someText= value;
notifyListeners();
}
}
Here is an example of how I am trying to build a base class, but am uncertain as to how I go about connecting it to Provider: (The idea here is that the below widget would be part of a more complex widget that would have a view_model where the state for the overall widget would be maintained)
class BaseCheckBoxTile extends StatefulWidget{
bool isChecked;
Function(bool) checkBoxOnChanged;
BaseCheckBoxTile({this.isChecked = false, required this.checkBoxOnChanged});
#override
_BaseCheckBoxTileState createState() => _BaseCheckBoxTileState();
}
class _BaseCheckBoxTileState extends State<BaseCheckBoxTile>{
#override
Widget build(BuildContext context) {
return SizedBox(
child: Checkbox(value: widget.isChecked,onChanged: widget.checkBoxOnChanged,),
);
}
}
class CustomCheckBoxTile extends BaseCheckBoxTile{
bool isChecked;
Function(bool) checkBoxOnChanged;
CustomCheckBoxTile({this.isChecked =false, required this.checkBoxOnChanged})
:super(isChecked: isChecked, checkBoxOnChanged: checkBoxOnChanged);
}
My instinct is to want to put something in my _BaseCheckBoxTileState that gives me access to the larger widget's view_model, like what I do in the first example with:
"MyScreenViewModel model = serviceLocator<MyScreenViewModel>(); "
If I had that, then I could assign the values in my _BaseCheckBoxTileState by referring to the model instead of widget (e.g., model.isChecked instead of widget.isChecked). The model would obviously extend ChangeNotifier, and the view that is making use of the custom widget would wrap the widget in a Consumer. However, the _BaseCheckBoxTileState doesn't know what view_model to listen to.
Would I accomplish this by putting some generic Type or Object in for my View_Model which could be assigned when the class is built? Or am I approaching this in a completely wrong way?
I have a Flutter App with Provider as a State Manager. The ChangeNotifierProvier is at the very top of my app (ie. above MaterialApp widget).
I have a ChangeNotifier class as follows:
class AmountManager extends ChangeNotifier {
String amount;
void changeAmount(String newAmount) {
amount = newAmount;
notifyListeners();
}
}
Then I have another class with a TextField :
class MyTextField extends StatelessWidget {
#override
Widget build(BuildContext context) {
return TextField(
//some decorations here
onChanged(value) {
Provider.of<AmountManager>(context).changeAmount(value);
},
);
}
}
And in another class under the Main App, I call the amount variable:
class MyText extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Text(
Provider.of<AmountManager>(context).amount,
);
}
}
The problem is the Provider.of(...) method cannot be called. I watched some tutorials and couldn't find out the reason behind it. If I use static text instead of the AmountManager object, it works. The program only uses the initial value of amount in MyText class.
What do you think I'm wrong with?
Thank you in advance,
You need listen: false when you use Provider.of() in onChanged.
onChanged: (value) {
Provider.of<AmountManager>(context, listen: false).changeAmount(value);
},
See this for details.
Is passing a GlobalKey down the tree using an InheritedWidget an antipattern? The stateful widget using that key is re-created (i.e. a new state this initState/disposed) every time its subtree is re-built.
My InheritedWidget looks like:
import 'package:flutter/material.dart';
import '../widgets/carousel.dart';
import '../widgets/panel/panel.dart';
class _CarouselKey extends GlobalObjectKey<CarouselState> {
const _CarouselKey(Object value) : super(value);
}
class _ProgressiveChatHeaderKey extends GlobalObjectKey<PanelScaffoldState> {
const _ProgressiveChatHeaderKey(Object value) : super(value);
}
class DimensionScopedKeyProvider extends InheritedWidget {
final _CarouselKey parallelBubbleCarouselKey;
final _ProgressiveChatHeaderKey progressiveChatHeaderKey;
final String keyString;
DimensionScopedKeyProvider({
Key key,
#required this.keyString,
#required Widget child,
}) : parallelBubbleCarouselKey = _CarouselKey(keyString),
progressiveChatHeaderKey = _ProgressiveChatHeaderKey(keyString),
super(key: key, child: child);
static DimensionScopedKeyProvider of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(DimensionScopedKeyProvider)
as DimensionScopedKeyProvider);
}
#override
bool updateShouldNotify(DimensionScopedKeyProvider oldWidget) => oldWidget.keyString != keyString;
}
And this InheritedWidget is rendered with a constant keyString, meaning that 1) updateShouldNotify always returns false and 2) the hashCode of the GlobalKeys passed to my build methods via DimensionScopedKeyProvider.of() are always identical.
The stateful widget builds something like
GlobalKey<PanelScaffoldState> get _headerKey => //
DimensionScopedKeyProvider.of(context).progressiveChatHeaderKey;
// ...
PanelScaffold(
key: _headerKey,
// ...
)
When I change a property that affects the subtree that the PanelScaffold lives in, though, a new PanelScaffoldState is created and the old one is disposed, even though the widget tree hasn't changed structure and the _headerKey hasn't changed either.
I also able to solve this problem, but I have no idea why it works.
The solution is to cache the access to the GlobalKey in didChangeDependencies
#override
void didChangeDependencies() {
super.didChangeDependencies();
_headerKey ??= DimensionScopedKeyProvider.of(context).progressiveChatHeaderKey;
}
.... and now everything is working as expected again—the rebuilds re-parent the existing state.
Does anyone know why caching the getter to the GlobalKey is the key here?