Does StatefulWidget not rebuild State every time it has new data? - flutter

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);
}

Related

Child widget not updating parent widget when child parameter changes

I have a child stateful widget that isn't updating the parent widget when an update to one of the parameters occurs.
The parent widget (OtherListVIew) is used to display a list of items on a PaginatedGrid with each item having a checkbox. The problem is that when a user views the page and there are selected items that are retrieved by the _retrieveItems() the changes do not reflect since at this point the ui is already rendered on the screen with the original empty list. These retrieved values do not reflect on OtherListView (_selectedItems is then used to display the checked items).
In this child widget I have the code
List<String> _selectedItems= [];
#override
void didChangeDependencies() {
super.didChangeDependencies();
WidgetsBinding.instance.addPostFrameCallback((_) {
_retrieveItems(); // this function retrieves items and reinitializes _selectedItems
});
}
#override
Widget build(BuildContext context) {
return SomeListView(
key: _listViewKey,
selectedItems: _selectedItems,
);
}
SomeListView looks as below
class SomeListView extends OtherListView {
const SomeListView ({super.key, super.selectedItems});
#override
State<SomeListView > createState() => SomeListViewState();
}
class SomeListViewState extends OtherListViewState<SomeListViewState> {
// override some method here from OtherListView for customization purposes
#override
Widget build(BuildContext context) {
return super.build(context);
}
}
OtherListView is the parent class that contains the PaginatedGrid that renders the passed _selectedItems to the UI. Why is it that after the _selectedItems have been refetched they are not reflecting on OtherListView? Is this an issue with my state management?
I have attempted using didUpdateWidget under OtherListView and indeed I can see the new values under the newWidget but they are not rendered on the UI. What I'm I missing?
#override
void didUpdateWidget(covariant T oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedItems != oldWidget.selectedItems) {
log.info('OtherListView selected items have been updated.');
}
}
OtherListView extends StatefulWidget {
final List<String>? selectedItems;
const OtherListView({Key? key,this.selectedItems})
: super(key: key);
// ...
}
honestly i don't understand your code but the problem you are facing the parameter is not updating because with setState it will only rebuild the parameter/state/variables that are in the same screen/class widget where the setState is called so you have to raise your parameter to the parent screen then pass it to the child with constructor and create a constructor in your child for function onTap for example then in the parent screen you use that onTap and called setState inside

How should I implement the init method? In a stateful or stateless widget?

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!!

Flutter & Dart : What is mounted for?

I am seeing this mounted syntax. What is it for? Could you give me sample?
TL;DR: A widget is mounted if it has state. If the widget is no longer mounted, i.e it has been closed or disposed, its state can no longer be updated. Therefore, we check if a widget is mounted to determine its state can still be updated.
Mounting is the process of creating the state of a StatefulWidget and attaching it to a BuildContext.
Take the following example:
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
#override
Widget build(BuildContext context) {
return Container(
);
}
}
The widget is assigned its state (_ExampleState) when the createState() method is called.
As soon as it is assigned its state, the widget becomes mounted.
Why is that important?
When a widget is unmounted in the dispose method of a StatefulWidget, it loses its state. This happens when it is no longer in the tree. I.e, it is has been closed, or no longer exists.
#override
void unmount() {
super.unmount();
state.dispose();
assert(() {
if (state._debugLifecycleState == _StateLifecycle.defunct)
return true;
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('${state.runtimeType}.dispose failed to call super.dispose.'),
ErrorDescription(
'dispose() implementations must always call their superclass dispose() method, to ensure '
'that all the resources used by the widget are fully released.'
),
]);
}());
// This is the key
state._element = null;
}
This basically means the state can't be updated and setState can no longer be called. So when you check if a widget is mounted, you're checking if its state can still be updated.
Use case:
Going back to our example Stateful Widget example, let's say we had a number that we wanted to update 30 seconds after the Widget is created.
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
int count = 0;
#override
void initState() {
Future.delayed(const Duration(seconds: 30), () {
setState(() => count = 5);
});
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('count $count'),
));
}
}
Our code will work fine, as long as the widget is disposed of or closed. If it is disposed of, we will get the famous error:
setState() called after dispose()
To prevent this, all we have to do is check if our widget still has state before updating it.
#override
void initState() {
Future.delayed(const Duration(seconds: 30), () {
if (mounted) setState(() => count = 5);
});
super.initState();
}
It represents whether a state is currently in the widget tree.
https://api.flutter.dev/flutter/widgets/State/mounted.html
You shouldn't call setState() on a state that is not currently in the tree.
Edit: The other answer provides a simple example. I should also mention that the described behavior is evident from the StatefulWidget lifecycle: https://flutterbyexample.com/lesson/stateful-widget-lifecycle
It's opinionated, but as far as I can see, it's a rare ocasion when you have to check for mounted, because you unsubscribe from outside events in dispose(). Even the Future from the example could be wrapped in CancelableOperation to cancel it in dispose(), which is before mounted == false

Flutter - animation widget, trigger animation only once but get data upates on widget

I came across the following problem already twice within my app:
Inside a StatefulWidget I have a widget that contains an animation and additionally displays other dynamic data. The animation is triggered after the animation widget has been built. However, after the animation (the drawing of a figure) has been played, I want to maintain it in the same drawn state. The rest of the dynamic data of the widget should be always rebuilt when the parent StatefulWidget's build method is called.
Below, there is a simple example of what I try to achieve:
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key, #required this.data}) : super(key: key);
CustomData data;
#override
State<StatefulWidget> createState() => _MyState();
}
Now, I had two ideas of how to implement the state:
1) Initialize the animation widget in initState() and "manually" handle data updates within didUpdateWidget().
class _MyState extends State<MyStatefulWidget> {
AnimationWidget _animationWidget;
#override
void initState() {
super.initState();
_animationWidget = AnimationWidget(displayData: widget.data);
// starts the animation when the widget is built (?)
WidgetsBinding.instance
.addPostFrameCallback((_) => _animationWidget.startAnimation());
}
#override
Widget build(BuildContext context) {
return Column(
children: [
_animationWidget,
... // other content
],
... // column size etc.
);
}
#override
void didUpdateWidget(SackenNotebookContent oldWidget) {
super.didUpdateWidget(oldWidget);
if(dataChanged(oldWidget.data, widget.data))
setState(() {
_animationWidget = AnimationWidget(displayData: widget.data);
});
}
}
2) To create the animation widget within the build() method. But then I already have doubts about when/where to trigger the animation to start. One way would be to trigger it in didUpdateWidget(). But again, the animation would then be triggered on every rebuild of the widget...
class _MyState extends State<MyStatefulWidget> {
#override
Widget build(BuildContext context) {
var animationWidget = AnimationWidget(displayData: widget.data);
// but where to call animationWidget.startAnimation() ?!
// when I call it here I get a null pointer exception...
return Column(
children: [
animationWidget,
... // other content
],
... // column size etc.
);
}
}
Has anyone got an idea of how I could handle such a problem in flutter?

streambuilder is rebuilding again and again when keyboard popup or closes

Here I was stuck in a problem. I have a column of widgets with a stream builder and a text field. When i try to input some text, the keyboard pops up and then the stream builder rebuilds again or when the keyboard closes, the stream builder rebuilds again. As i am building a chat screen, I don't want to rebuild the stream builder again as it increases in number of reads.
Any sort of suggestions helpful.
Flutter calls the build() method every time it wants to change
anything in the view, and this happens surprisingly often.
You can pass the stream into the stateless widget
MyApp({Key key, this.stream}) : super(key: key);
Or build the stream in the initState method if the widget is statefull.
#override
void initState() {
super.initState();
post = buildStream();
}
What #TuanNguyen means by
build the stream in the initState method
is the following, if for you are using Firestore for exemple:
class MyStateFullWidget extends StatefulWidget {
const MyStateFullWidget({Key key}) : super(key: key);
#override
_MyStateFullWidgetState createState() => _MyStateFullWidgetState();
}
class _MyStateFullWidgetState extends State<MyStateFullWidget> {
Stream _myStream;
#override
void initState() {
super.initState();
_myStream = FirebaseFirestore.instance.collection(myCollection) ... .snapshots();
}
#override
Widget build(BuildContext context) {
return SomeUpperWidget(
child:
StreamBuilder(
stream: _myStream,
builder: (ctx, snap) => ... ,
)
);
}
}
I was facing the same issue. I could not find simple alternative to avoid re-rendering without changing much code. So I ended up like this:
So in the Bloc class, first initiated a variable say
streamStateIndex = 0;
And wherever I am using
sink.add(data)
, I started using
streamStateIndex++;
sink.add({"data": data, "streamStateIndex":streamStateIndex});
And initiated another variable lets say localStreamStateIndex = 0; inside Stateful flutter class to compare streamStateIndex from bloc
And used inside StreamBuilder like this:
if(snapshot.hasData){
if(localStreamStateIndex < snapshot.data['streamStateIndex']){
updateLocalState(snapshot.data['data']);
localStreamStateIndex = snapshot.data['streamStateIndex'];
}
}