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?
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!!
The widget TrainsPage is added to the build graph in main.dart, when the corresponding menu button is clicked. This is done twice: once when _routes is empty and a second time when _routes is filled.
Widget pageSelector() {
if (_selectedIndex == 2) {
return new TrainsPage(routes: _routes);
} else
return Text("");
}
In TrainsPage.dart, I have the code for the stateful widget TrainsPage.
class TrainsPage extends StatefulWidget {
const TrainsPage({Key? key, required this.routes}) : super(key: key);
final List<RSRoute> routes;
#override
_TrainsPageState createState() => _TrainsPageState();
}
class _TrainsPageState extends State<TrainsPage> {
List<RSRoute> _routes = List.empty();
#override
void initState() {
super.initState();
this._routes = new List<RSRoute>.from(widget.routes);
Now, the second time, TrainsPage gets called in main.dart (now with routes filled), initState() of _TrainsPageState is not called, which is responsible to read the data in routes. And because routes was empty the first time, there is nothing in display on the trains page.
Why does TrainsPage not rebuild _TrainsPageState, when it clearly got new data in the constructor?
This is exactly why the State exists : to keep the state of the current context alive even when the widget is rebuild.
If it was recreated each time the statefull widget is rebuild it could not keep the state of its own variables.
class MyWidget extends StatelessWidget {
var _someStateVariable = 0;
#override
void build(BuildContext context){
// here an action that increment _someStateVariable
}
}
Here _someStateVariable would be reset to 0 at each rebuild. Or if we wanted a StateFullWidget in the first place it's because we'll update this variable later and want to keep its updated value through the multiple widget rebuilds.
If you don't have such state variable to maintain maybe you don't need a StateFullWidget here.
Now to the solution to your problem : you can override didUpdateWidget instead of initstate since it will be called at each widget rebuild :
#override
void didUpdateWidget() {
didUpdateWidget();
_routes = new List<RSRoute>.from(widget.routes);
}
Consider the following code snippet:
class CustomStateFulWidget extends StatefulWidget {
final GlobalKey<NavigatorState> navigatorKeyFinal = GlobalKey();
#override
_CustomStateFulWidget createState() => _CustomStateFulWidget();
}
class _CustomStateFulWidget extends State<CustomStateFulWidget> {
GlobalKey<NavigatorState> navigatorKeyLocal = GlobalKey();
#override
Widget build(BuildContext context) {
print("Navigator key local: ${navigatorKeyLocal.hashCode}");
print("Navigator key final: ${widget.navigatorKeyFinal.hashCode}");
return Center();
}
}
In the above code for every time the build method of _CustomStateFulWidget called, the navigatorKeyFinal variable value changes, but the navigatorKeyLocal variable stay unchanged. Can anyone explain me the reason of this behaviour?
I do not understand the constructor part and static function part.
super?
dependOnInheritedWidgetOfExactType?
import 'comments_bloc.dart';
export 'comments_bloc.dart';
class CommentsProvider extends InheritedWidget {
final CommentsBloc commentsBloc;
CommentsProvider({Key key, Widget child})
: commentsBloc = CommentsBloc(), // what this line is doing.
super(key: key, child: child);
bool updateShouldNotify(_) => true;
//below code?
static CommentsBloc of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CommentsProvider>()
.commentsBloc;
}
}
Step 1 : The dependOnInheritedWidgetOfExactType method enables a descendant widget to access the closest ancestor InheritedWidget instance enclosed in its BuildContext, in your code is CommentsProvider
Step 2 : And .commentsBloc means access this CommentsProvider 's attribute, in your code is final CommentsBloc commentsBloc;
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,
);
}
}