Call setState outside of a StatefulWidget (with a setter) - flutter

I have a lot of experience with Javascript and now I want to start with Flutter. So maybe my problem is that I see the code as JS developer and the Flutter solution would be different. But here is my problem:
I want to create a simple card game. A playing card is a StatefulWidget:
class GameCard extends StatefulWidget {
int _value;
String _suit;
bool _visible;
String _imagePath;
// void setVisible() {
// How to execute setState here??
// }
GameCard({int value, String suit, bool visible = false}) {
...
this._visible = visible;
this._value = value;
this._suit = suit;
}
#override
_GameCardState createState() => _GameCardState();
}
class _GameCardState extends State<GameCard> {
String getSuit() => widget._suit;
int getValue() => widget._value;
bool isVisible() => widget._visible;
#override
Widget build(BuildContext context) {
...
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
width: width,
height: width * 1.5,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage(widget._imagePath),
...
The idea is that the imagePath in the state class should be a image of a card back when the card is not visible.
Now I want to have a setVisible function in my StatefulWidget which runs setState() followed by an update of the imagePath. After that imagePath should be a image of a playing card like for example hearts king.
So finally I want to create an instance of GameCard somewhere else and change the image with a setter:
GameCard spadeFour = GameCard(value: 4, suit: 'spade', visible: false),
spadeFour.setVisible(true)
How can I do that? I read about Globalkey and other stuff but it can't be that complicated cause this is a very common use case.

Generally you should avoid instantiating a widget and then modifying it through properties. This is also unnecessary since you already correctly use parameters to change the appearance of the widget.
To get the widget to redraw with different parameters, instead of calling setState within that widget, call it in the parent widget.
class _YourParentPageState extends State<YourParentPage> {
bool visible = false;
_changeVisibility(bool v) {
setState(() {
visible = v;
});
}
#override
Widget build(BuildContext context) {
return GameCard(value: 4, suit: 'spade', visible: visible);
}
}
Calling _changeVisibility will redraw your GameCard with the updated visible parameter.

Related

Controlling state of a widget from another widget

I'm using the example in this link:
https://medium.com/#jb.padamchopra/implementing-a-bottom-app-bar-with-floating-action-button-flutter-463560e1324
Widget from the link
The floating action button animation is controlled by a bool clickedCenterFAB
In my code the Widgets inside the animated container are defined in a class and I want, after the user writes a text and presses the button, the animated container to collapse, so logically I want to call setState and assign clickedCenterFAB = false.
I'm new to flutter and I tried calling setState inside the uploadPage widget and try to change clickedCenterFAB but it didn't work. Any idea how to do it guys? thanks for the help in advance.
My widget
AnimatedContainer(
duration: Duration(milliseconds: 300),
//if clickedCentreFAB == true, the first parameter is used. If it's false, the second.
height: clickedCentreFAB
? MediaQuery.of(context).size.height
: 10.0,
width: clickedCentreFAB
? MediaQuery.of(context).size.height
: 10.0,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(clickedCentreFAB ? 0.0 : 300.0),
color: Colors.blue,
),
child: clickedCentreFAB ? UploadPage() : null)
EDIT: After following with Michael's solution and keeping up with the comments, this is where I got
class UploadPage extends StatefulWidget {
final Function collapseContainer;
UploadPage(this.collapseContainer);
#override
_UploadPageState createState() =>
_UploadPageState(collapseContainer);
}
class _UploadPageState extends State<UploadPage> {
final Function collapseContainer;
_UploadPageState(this.collapseContainer);
...
...
CustomButton(
() {
if (_controller.text.trim().isNotEmpty) {
DatabaseServices().uploadPost(
FirebaseAuth.instance.currentUser.uid,
_controller.text,
!_isChecked);
collapseContainer;
}
},
'Post',
Colors.white,
),
CustomButton obviously is a widget class that i created to avoid redundency and in the params in a Function which I'm passing here
The reason you can't call setState directly from UploadPage is because setState is a class method, so it's always only associated with the state of the immediately enclosing class instance. If you want to change the state of a Widget that's higher up in the Widget tree, the standard method is to pass a callback to the child component.
In your case, you might add a new property to your UploadPage widget - something like, onButtonPressed:
class UploadPage extends StatelessWidget {
final Function onButtonPressed;
UploadPage({required this.onButtonPressed});
#override
Widget build(BuildContext context) {
return YourWidget(
// ...
child: RaisedButton(onPressed: onButtonPressed),
// ...
);
}
}
Then:
AnimatedContainer(
// ...
child: clickedCentreFAB
? UploadPage(
onButtonPressed: () {
setState(() { return clickedCentreFAB = false; });
},
)
: null,
)
)
As an aside, at a certain point, the state of your app may be complex enough to warrant solutions like bloc, redux, etc - However, for this particular scenario those might be overkill.
If you find yourself having to pass callbacks and properties through several levels of the widget tree, then provider can be immensely helpful.
If the mapping between actions, states, and views becomes very complex, then bloc or redux can help to impose structure onto your project and make it easier to reason about, though you will often need to write more code to get the same tasks done.

flutter slider not updating widget variables

am playing around with the slider widget on flutter, and I can't figure out why it does not update certain values in a different widget, example code is shown below;
When i move the slider, it has no issues moving, but the value i'm trying to update on the other widget does not update even though the onchanged is updating the variable passed through in a set state accordingly.
any help would be greatly appreciated!
Scaffold Code
class TestPage extends StatelessWidget {
static const id = "test_page";
#override
Widget build(BuildContext context) {
double testValue = 0;
return Scaffold(
body: Column(
children: [
Text("Hello World"),
TestBoxNumber(
numberDisplay: testValue,
),
TestSlider(testValue: testValue),
],
),
);
}
}
Slider Code
class TestSlider extends StatefulWidget {
double testValue;
TestSlider({required this.testValue});
#override
_TestSliderState createState() => _TestSliderState();
}
class _TestSliderState extends State<TestSlider> {
#override
Widget build(BuildContext context) {
return Slider(
activeColor: themeData.primaryColorLight,
value: widget.testValue,
min: 0,
max: 100,
divisions: 100,
label: widget.testValue.round().toString(),
onChanged: (double value) {
setState(() {
widget.testValue = value;
});
},
);
}
}
Different Widget Code
class TestBoxNumber extends StatefulWidget {
final double numberDisplay;
const TestBoxNumber({required this.numberDisplay});
#override
_TestBoxNumberState createState() => _TestBoxNumberState();
}
class _TestBoxNumberState extends State<TestBoxNumber> {
#override
Widget build(BuildContext context) {
return Container(
child: Text(widget.numberDisplay.toString()),
);
}
}
The problem is that you are constructing TestBoxNumber widget in such a way that value (testValue) will always be the same (testValue is never returned out of the TestSlider widget).
How to overcome this issue?
You can make your TestPage a StatefullWidget. Then create callback from TestSlider, so when you change value in TestSlider you will call some function in TestPage (with setState in it, causing re-rendering your page).
Or if you don't want your whole TestPage widget to be Statefull (if, let's say, you predict a lot of other static widgets in it and you don't want them to be re-rendered because you just moved a slider), you can create wrapper Statefull widget and put both TestSlider and TestBoxNumber widgets in it. This is more flexible approach, imho.
Here is small scheme of what I mean by wrapping two widgets in another one:
UPD: btw, there is no point in making TestBoxText a statefull widget if it's only purpose is to display a text and you pass it's value through the constructor.

List.remove() removes the correct object but the object's state in the list shifts as if last entry is being deleted

So I have been trying to solve this issue for a couple of days now and I seem to have hit a real dead end here.
The issue (which I have simplified in the code below) is that when I try to remove an item from a List<"SomeDataObject">, it does actually remove the correct object from the list. This is evident because the ID that I arbitrarily assigned to the objects does shift just as I would expect it to when I remove something. However, oddly enough, even though the IDs shift in the list and the correct ID is removed, the states of all the widgets seem to act as though the last item is always removed, even when the deleted object is at the middle or even beginning of the list.
An example would be if the list looked as such:
Data(id: 630) // This one starts blue
Data(id: 243) // Let's say the user made this one red
Data(id: 944) // Also blue
And let's say I tried to remove the middle item from this list. Well what happens is that the list will look like this now:
Data(id: 630)
Data(id: 944)
This seems at first to be great because it looks like exactly what I wanted, but for some reason, the colors did not change their order and are still Red first, then Blue. The color state data seems to function independently from the actual objects and I could not find a clear solution.
I have code of an example program to reproduce this problem and I also have some pictures below of the program so that it is clearer what the issue is.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: HomeScreen(),
);
}
}
// Home screen class to display app
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
List<DataWidget> widgetsList = List<DataWidget>();
// Adds a new data object to the widgets list
void addData() {
setState(() {
widgetsList.add(DataWidget(removeData));
});
}
// Removes a given widget from the widgets list
void removeData(DataWidget toRemove) {
setState(() {
widgetsList = List.from(widgetsList)..remove(toRemove);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
/* I used a SingleChildScrollView instead of ListView because ListView
resets an objects state when it gets out of view so I wrapped the whole
list in a column and then passed that to the SingleChildScrollView to
force it to stay in memory. It is worth noting that this problem still
exists even when using a regular ListView. */
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// Added a SizedBox just to make column take up whole screen width
children: [...widgetsList, SizedBox(width: double.infinity)],
),
),
// FloatingActionButton just to run addData function for example
floatingActionButton: FloatingActionButton(onPressed: addData),
);
}
}
// Simple class that is a red square that when clicked turns blue when pressed
class DataWidget extends StatefulWidget {
DataWidget(this.onDoubleTap);
final Function(DataWidget) onDoubleTap;
// Just a random number to keep track of which widget this is
final String id = Random.secure().nextInt(1000).toString();
#override
String toStringShort() {
return id;
}
#override
_DataWidgetState createState() => _DataWidgetState();
}
class _DataWidgetState extends State<DataWidget> {
/* Here the example state information is just a color, but in the full version
of this problem this actually has multiple fields */
Color color = Colors.red;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
// onDoubleTap this passes itself to the delete method of the parent
onDoubleTap: () => widget.onDoubleTap(widget),
// Changes color on tap to whatever color it isn't
onTap: () {
setState(() {
color = color == Colors.red ? Colors.blue : Colors.red;
});
},
child: Container(
child: Center(child: Text(widget.id)),
width: 200,
height: 200,
color: color,
),
),
);
}
}
Before user changes any colors (object ID is displayed as text on container):
User changes middle container to blue by tapping:
User attempts to delete middle (blue) container by double tapping:
As you can see from the image above, the ID was deleted and the ID from below was moved up on the screen, but the color information from the state did not get deleted.
Any ideas for things to try would be greatly appreciated.
You need to make a few changes to your DataWidget class to make it work:
// Simple class that is a red square that when clicked turns blue when pressed
class DataWidget extends StatefulWidget {
Color color = Colors.red; // moved from _DataWidgetState class
DataWidget(this.onDoubleTap);
final Function(DataWidget) onDoubleTap;
// Just a random number to keep track of which widget this is
final String id = Random.secure().nextInt(1000).toString();
#override
String toStringShort() {
return id;
}
#override
_DataWidgetState createState() => _DataWidgetState();
}
class _DataWidgetState extends State<DataWidget> {
/* Here the example state information is just a color, but in the full version
of this problem this actually has multiple fields */
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
// onDoubleTap this passes itself to the delete method of the parent
onDoubleTap: () => widget.onDoubleTap(widget),
// Changes color on tap to whatever color it isn't
onTap: () {
setState(() {
widget.color =
widget.color == Colors.red ? Colors.blue : Colors.red;
});
},
child: Container(
child: Center(child: Text(widget.id)),
width: 200,
height: 200,
color: widget.color,
),
),
);
}
}

How to change values within a stateful widget class from a different class?

I have a stateful widget LetterButton()
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text(widget.caption),
onPressed: onChanged,
color: colors[currentIndex],
padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
);
}
In my main.dart file I declare an array of LetterButtons
List<LetterButton> buttonArray;
which I initialize and fill during initState() via the method
void makeButtons() {
for (var letter in alphabet) {
buttonArray.add(
LetterButton(letter),
);
}
}
The buttons in the list are then displayed in the UI
Wrap(
children: buttonArray,
)
How can I change the value of currentIndex (an int in
class LetterButtonState extends State<LetterButton>) or otherwise change all the buttons to the same color from main.dart?
NOTE: I asked a similar question a few days ago, but the answer was a little above my current knowledge, as are responses I've seen to similar Q's here on SO. I have a little understanding of callbacks, and experimented a little with the provider package, but there's such a variety of answers and info available online that it's hard for me to even know what I don't know to be able to answer my question :-)
Create stateful widget with state as public access, so that you can access outside of the package and provide key to constructor. So that you can refer key and get can get state to change value. See the following example
class LetterButton extends StatefulWidget {
LetterButton({GlobalKey key}) : super(key: key);
#override
LetterButtonState createState() => LetterButtonState();
}
class LetterButtonState extends State<LetterButton> {
int value = 0;
//this public method is to update int value
setValue(int value) {
setState(() {
this.value = value;
});
}
#override
Widget build(BuildContext context) {
return Center(
child: Text(value.toString()),
);
}
}
//In Main.dart
GlobalKey<LetterButtonState> _buttonStateKey = GlobalKey();
//while creating widget
LetterButton(key:_buttonStateKey)
//in onTapCallback you can call to update value
_buttonStateKey.currentState?.setValue(10);
Just send your created function to new class by parameter and the new class should be Constarcter with Function lThen you can call the function from a new class.

Why doesn't the build method of child get called when parent state changes in flutter?

Here's my code in which i am running the move method on the snake object every second which changes the state of the snake (The snake is a normal class and just has some primitive types which get updated ) and calling the setState of the SnakeBoard class . The issue is that , i was expecting it to call the build method of even BoardTile class since it uses the same snake object which got changed inside the snake object.
Since the build method of BoardTile class doesn't get called at each second interval , the snake never moves. Why is this happening ? Please explain ....
class SnakeBoard extends StatefulWidget {
List<BoardTile> _boardTiles ;
List<BoardTile> get boardTiles => _boardTiles;
set boardTiles(List<BoardTile> boardTiles) {
_boardTiles = boardTiles;
}
Snake _snake ;
Snake get snake => _snake;
set snake(Snake snake) {
_snake = snake;
}
SnakeBoard(){
boardTiles = List.generate( 100 , (int index)=> BoardTile( index , this ) ) ;
snake = Snake(10 , 10) ;
}
#override
_SnakeBoardState createState() => _SnakeBoardState();
}
class _SnakeBoardState extends State<SnakeBoard> {
#override
Widget build(BuildContext context) {
return Container(
child : GridView.count(crossAxisCount: 10,
shrinkWrap: true,
children: widget.boardTiles
),
);
}
#override
void initState() {
Timer.periodic(Duration(seconds: 2), (timer){
setState(() {
widget.snake.move() ;
print("Snaking moving !" ) ;
});
});
}
}
class BoardTile extends StatefulWidget {
final int id ;
final SnakeBoard snakeBoard ;
#override
_BoardTileState createState() => _BoardTileState();
BoardTile(this.id, this.snakeBoard);
}
class _BoardTileState extends State<BoardTile> {
#override
Widget build(BuildContext context) {
Snake snake = widget.snakeBoard.snake ;
return Container(
decoration: BoxDecoration( border: Border.all(width: 1) ),
child:
snake.isPresent(widget.id) ? snake.getSnakePartWidget(widget.id) : Text(widget.id.toString()) ,
);
}
}
Without running your code, my general advise is to try and move the fields/attributes that make up your "state" (here: the Snake) from the SnakeBoard to the _SnakeBoardState class.
And to specifically address your question:
It would be reasonable for Flutter not to rebuild the BoardTiles (but instead reuse the old ones) because the snake is not part of the BoardTiles' state -- for that it would need to be a field in your _BoardTileState class.
Thus from Flutter's point of view the BoardTile's state hasn't changed.
You should call setState inside build method of BoardTile class instead of initState method SnakeBoard class.