setState not updating variable when declared on initState - flutter

I have run into an issue with Flutter: I have a navbar which, depending on the item selected, returns me an integer number called Index. This index is then passed through a List to get the content of the body of the Scaffold, an object of Widget() class.
The default body is an object of HomePage(), that has an integer parameter called rpm. By default, the HomePage() should be the body displayed, so, as it depends on the rpm parameter, I declare the rpm parameter on the initState. I also have a setState that changes dynamically the rpm.
The weird thing is: if I declare the List<Widgets> bodyList in the initState, the setState doesn't seem to work. However, if I declare List<Widgets> bodyList in the build method, I can see the content of the HomePage() change dynamically with the rpm.
An excerpt of the code. Not working:
class _ScreenTreeState extends State<ScreenTree> {
int _index;
int _rpm;
List<Widget> bodyList;
#override
void initState() {
_isPlaying = false;
_rpm = 0;
_index = 0;
bodyList = [
HomePage(rpm: _rpm),
StatisticsScreen(),
WeightScreen(),
SettingsPage()
];
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(...
Working:
class _ScreenTreeState extends State<ScreenTree> {
int _index;
int _rpm;
List<Widget> bodyList;
#override
void initState() {
_isPlaying = false;
_rpm = 0;
_index =
0;
super.initState();
}
#override
Widget build(BuildContext context) {
bodyList = [
HomePage(rpm: _rpm),
StatisticsScreen(),
WeightScreen(),
SettingsPage()
];
return Scaffold(...
My theory is that this may have to do with the fact that if _rpm is declared as an integer, Dart reads it as a primitive and not as the object of an integer class, so if declared on the initState, I'm not actually passing an object, but a primitive. But I don't understand then why is it working when declared in the build method.
I feel this is irrelevant, but if somebody needs it, here it is the code that updates the value of _rpm. Please, don't read too much into this because I feel it is actually not relevant and may distract and confuse more than help:
onPressed: () {
if (_isPlaying) {
setState(() {
_isPlaying = false;
});
Provider.of<MicrophoneEngine>(context, listen: false)
.stopRecording();
} else {
setState(() {
_isPlaying = true;
});
Provider.of<MicrophoneEngine>(context, listen: false)
.startRecording((rpmCall) {
setState(() {
_rpm = rpmCall;
});
});
}
},

It's a bad practice to keep references for the widgets(like in the first example), in fact, you don't need that List at all.
Every time you use setState, you rebuild the widgets and you pass the new value. In the first example you pass the HomePage(rpm: _rpm) from the init every time, that's why it is not updated. Meaning, you don't rebuild HomePage with the updated value, you just passing the HomePage from the initState which was instantiated with the initial _rpm value of 0.

Related

Flutter: How to share an instance of statefull widget?

I have a "WidgetBackGround" statefullwidget that return an animated background for my app,
I use it like this :
Scaffold( resizeToAvoidBottomInset: false, body: WidgetBackGround( child: Container(),),)
The problem is when I use navigator to change screen and reuse WidgetBackGround an other instance is created and the animation is not a the same state that previous screen.
I want to have the same animated background on all my app, is it possible to instance it one time and then just reuse it ?
WidgetBackGround.dart look like this:
final Widget child;
WidgetBackGround({this.child = const SizedBox.expand()});
#override
_WidgetBackGroundState createState() => _WidgetBackGroundState();
}
class _WidgetBackGroundState extends State<WidgetBackGround> {
double iter = 0.0;
#override
void initState() {
Future.delayed(Duration(seconds: 1)).then((value) async {
for (int i = 0; i < 2000000; i++) {
setState(() {
iter = iter + 0.000001;
});
await Future.delayed(Duration(milliseconds: 50));
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(painter: SpaceBackGround(iter), child: widget.child);
}
}
this is not a solution, but maybe a valid workaround:
try making the iter a static variable,
this of course won't preserve the state of WidgetBackGround but will let the animation continue from its last value in the previous screen
A valid solution (not sure if it's the best out there):
is to use some dependency injection tool (for example get_it) and provide your WidgetBackGround object as a singleton for every scaffold in your app

How to force stateful widget redraw using keys?

I'm making an app that pulls data from an API and displays it in a view (MVC style).
I need to figure out how to force my view widget to redraw itself. Right now I tried with ValueKeys and ObjectKeys but to no avail.
There's lots and lots of code so I am going to use snippets as much as possible to keep it clear. (If you need to see more code feel free to ask)
Here's my view widget:
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
Object redrawObject = Object();
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
this.redrawObject = widget.key;
super.initState();
}
}
You can see in the commented code that I am planning to change the way the view builds itself in function of the data that gets passed to it.
What I have tried so far is to pass a ValueKey/ObjectKey to the view from main.dart in a constructor and then changing the object at runtime. Unfortunately that did not work.
At the top of my main.dart(accessible from anywhere within main) I have this:
Object redraw = Object();
final dataView = new view(key: ObjectKey(redraw));
Then in the body of the homepage I have the view and a floating button right under.
If I press the button it should increment the counter inside the view and force it to redraw. Here's the code I have tried so far:
body: Center(
child: dataView
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
redraw = new Object();
},
),
From what I understand, if the object that was used as a key gets changed, then flutter should rebuild the state for the widget. So I'm setting my object to a new object but it's not working.
I also tried something like this:
onPressed: (){
setState((){
dataView.incrementCounter();
redraw = new Object();
});
},
Eventually I'd like to use a navigator in conjunction with my view widget (so that we have a back button) but I don't know if this is possible.
It feels a bit like I'm fighting with the framework. Is there a different paradigm I should use (like pages?) or is it possible for me to do it this way?
How do I force my view widget to get redrawn?
Using Göktuğ Vatandaş' answer and GlobalKeys I was able to figure it out.
I made a reDraw() function inside the state and then I called it from my main using a GlobalKey.
Note: Wrapping in a container and using a key for the container is not necessary. Calling setState() is enough to force a redraw.
Here's the new view widget:
import 'dart:convert';
import 'package:flutter/cupertino.dart';
GlobalKey<_viewState> viewKey = GlobalKey();
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
super.initState();
}
void reDraw(){
setState((){});
}
}
Here's where I declare the view widget in my main:
final dataView = new view(key: viewKey);
Here's where I call the reDraw() function:
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
viewKey.currentState.reDraw();
},
),
Thanks Göktuğ Vatandaş!
You can check flutter_phoenix's logic for redraw effect. I think its very useful or you can just use package itself. Basically it does what you trying to achive.
It creates a unique key in state.
Key _key = UniqueKey();
Injects it to a container.
#override
Widget build(BuildContext context) {
return Container(
key: _key,
child: widget.child,
);
}
And when you call rebirth it just refresh key and that causes view to rebuild.
void restartApp() {
setState(() {
_key = UniqueKey();
});
}

Why can't I call these calculations into my build, when I'm already doing it for another function

The var _distancebetween is causing issues here. I don't know how to call it in my widget Build:
void _onCameraMove(CameraPosition cameraPosition) async {
var _distancebetween = await Geolocator().distanceBetween(
lat, lng, _lastMapPosition.latitude, _lastMapPosition.longitude);
_lastMapPosition = cameraPosition.target;
var latlnglastPosition = _lastMapPosition.toString;
setState(() {
latlnglastPosition;
_distancebetween;
_lastMapPosition;
});
}
when I setState for _latlnglastPosition, and _lastMapPosition, I call them in my build like so:
#override
Widget build(BuildContext context) {
var latlnglastPosition = _lastMapPosition.toString();
and I can easily display the text in an Ink Response field.
Why won't this work for _distancebetweenFinal?
In my build:
var _distancebetweenFinal = _distancebetween.toString();
print(_distancebetweenFinal);
print(_distancebetween);
This prints zeros... But if printed from my void _onCameraMove, it's printing the correct value. How can I pass the value from _onCameraMove into my widget build?
Declare your _onCameraMove() in initState could work, like this:
#override
void initState() {
super.initState();
_onCameraMove();
}
And make sure you make your class Statefull and call initState before build method

How to prevent object from being reinitialized everytime it is invoked? - Dart / Flutter

For the snippet of the code below:
// snippet of the main class
class MainState extends State<Main>{
MusicMaterial musicObj = MusicMaterial();
SoundsMaterial soundObj = SoundsMaterial();
#override
Widget build(BuildContext context) {
return Container(
child: something.value == 0
? musicObj
: soundObj
);
}
}
// snippet of the MusicMaterial class
class MusicMaterialState extends State<MusicMaterial>{
#override
Widget build(BuildContext context) {
return Row(
AnotherClass obj1 = AnotherClass(0, 'test'),
AnotherClass obj2 = AnotherClass(1, 'test'),
);
}
}
// snippet of the AnotherClass class
class AnotherClassState extends State<AnotherClass>{
import '../globals.dart' as globals;
#override
void initState() {
globals.globalCounter++; // this variable is just a global variable from the globals.dart page
}
}
// snippet of the global.dart
library my_prj.globals;
globalCounter = 0;
It keeps creating a new instance every time the "if" state is updated in the Main State class. So for instance, the value of the global counter keeps going up from 0 to 2 to 4...8... How do we ensure that the object does not get re-initialized every single time, so for instance void initState() from AnotherClassState is called only once? i.e the value remains 2 and only 2.
I have tried using "AutomaticKeepAliveClientMixin and #override bool get wantKeepAlive => true" - i.e keeping it alive so when it is invoked next time, it does not call initState() again, however it did not work.
Hopefully I'm understanding correctly what you need. It seems that you want the counter to be increased only one time per class type. I'm sure there are different ways to do it but It comes to my mind to make globalCounter a little more complex
class GlobalCounter {
List<String> _keys = List<String>();
int _counter = 0;
int get counter => _counter;
void increaseCounter(String key) {
// increase only if the key passed as parameter didn't increase already
if (!_keys.contains(key)) {
_counter++;
_keys.add(key);
}
}
}
globalCounter = GlobalCounter();
Then you can use it like this
#override
void initState() {
// pass the type of the instance trying to increase the counter
globals.globalCounter.increaseCounter(this.runtimeType.toString());
}

Get InheritedWidget parameter in initState

i need some help understanding how to obtain data from inherited widget.
I usually get the parameter from my widget directly from the build method using
#override
Widget build(BuildContext context) {
//THIS METHOD
var data = StateContainer.of(context).data;
return Container(child:Text("${data.parameter}"));
}
But this method cant be called from initState since there is no buildContext yet.
I need in the initState method to have that parameter (i call my fetch from server in that and i need to pass that data to my function), so, how should i do it?
#override
void initState() {
otherData = fetchData(data);
super.initState();
}
I tried using didChangeDipendencies() but it is called every time the view is rebuilt (popping from screen, etc.) so it is not what i want to use and neither the FutureBuilder widget.
Any suggestion?
First, note that you probably do want to use didChangeDependencies. But you can't just do your call there without any check. You need to wrap it in an if first.
A typical didChangeDependencies implementation should look similar to:
Foo foo;
#override
void didChangeDependencies() {
super.didChangeDependencies();
final foo = Foo.of(context);
if (this.foo != foo) {
this.foo = foo;
foo.doSomething();
}
}
Using such code, doSomething will be executed only when foo changes.
Alternatively, if you are lazy and know for sure that your object will never ever change, there's another solution.
To obtain an InheritedWidget, the method typically used is:
BuildContext context;
InheritedWidget foo = context.inheritFromWidgetOfExactType(Foo);
and it is this method that cannot be called inside initState.
But there's another method that does the same thing:
BuildContext context;
InheritedWidget foo = context.ancestorInheritedElementForWidgetOfExactType(Foo)?.widget;
The twist is:
- this method can be called inside initState
- it won't handle the scenario where the value changed.
So if your value never changes, you can use that instead.
1, If you only need InheritedWidget as a Provider of parameter for Widget.
You can using on initState as bellow:
#override
void initState() {
super.initState();
var data = context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
}
2, If you need listener to re-render widget when data of InheritedWidget change. I suggest you wrapper your StatefulWidget insider a StatelessWidget,
parameter of StatefulWidget is passed from StatelessWidget, when InheritedWidget change data, it will notify to StatelessWidget, on StatefulWidget we will get change on didChangeDependencies and you can refresh data.
This is code guide:
class WrapperDemoWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
DemoData data = StateContainer.of(context).data;
return Container();
}
}
class ImplementWidget extends StatefulWidget {
DemoData data;
ImplementWidget({this.data});
#override
_ImplementWidgetState createState() => _ImplementWidgetState();
}
class _ImplementWidgetState extends State<ImplementWidget> {
#override
void initState() {
super.initState();
//TODO Do sth with widget.data
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
//TODO Do change with widget.data
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I prefer the solution with didChangeDependencies because Future.delayed solution is a bit hack, looks unprofessional and unhealthy. However, it works out of the box.
This is the solution I prefer:
class _MyAppState extends State<MyApp> {
bool isDataLoaded = false;
#override
void didChangeDependencies() {
if (!isDataLoaded) {
otherData = fetchData(data).then((_){
this.isDataLoaded = true;
});
}
super.didChangeDependencies();
}
...
You can also get the context in initState, try using a future with duration zero. You can find some examples here
void initState() {
super.initState();
Future.delayed(Duration.zero,() {
//use context here
showDialog(context: context, builder: (context) => AlertDialog(
content: Column(
children: <Widget>[
Text('#todo')
],
),
actions: <Widget>[
FlatButton(onPressed: (){
Navigator.pop(context);
}, child: Text('OK')),
],
));
});
}
i use it to make loading screens using inherited widgets and avoid some global variables