Flutter accessing State class variable of StatefulWidget from a different class - flutter

I have a StatefulWidget class 'FirstClass' which extends a State '_FirstClassState'. From a separate State class, called '_SecondClassState', I'm trying to access the value of a variable (called 'counter' in '_FirstClassState').
I've found a solution, but I'm not sure if this is the best solution to the problem. I've tried looking at other similar questions:
How to access Stateful widget variable inside State class outside the build method? (Declaring the variable in the StatefulWidget class and using 'widget.' in the State class does not allow me to get the value of 'counter' from '_SecondClassState')
I've also seen other sites that recommend using a GlobalKey. However, even though this might be a solution, I didn't explore further into this as I found other sites which recommend minimising the use of GlobalKeys
second_class.dart:
import 'package:brew_crew/question/first_class.dart';
import 'package:flutter/material.dart';
class SecondClass extends StatefulWidget {
#override
_SecondClassState createState() => _SecondClassState();
}
class _SecondClassState extends State<SecondClass> {
final FirstClass firstClass = FirstClass();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("MyApp"),
centerTitle: true,
),
body: Column(
children: <Widget>[
firstClass, //Displays the button to increase 'counter'
FlatButton(
child: Text("Submit"),
onPressed: () {
print(firstClass
.getCounter()); //I need to get the value of 'counter' here to use elsewhere
}),
],
),
);
}
}
first_class.dart
import 'package:flutter/material.dart';
class FirstClass extends StatefulWidget {
final _FirstClassState firstClassState = _FirstClassState();
#override
_FirstClassState createState() => firstClassState;
int getCounter() {
return firstClassState.counter;
}
}
class _FirstClassState extends State<FirstClass> {
int counter = 0;
//Increases counter by 1
void _increaseCounter() {
setState(() {
counter++;
});
}
#override
Widget build(BuildContext context) {
//A button which increases counter when clicked
return FlatButton.icon(
icon: Icon(Icons.add),
label: Text("Counter: $counter"),
onPressed: () {
_increaseCounter();
},
);
}
}
As you can see, within 'FirstClass', I initialise a new instance of '_FirstClassState'. Using this in the 'getCounter()' function, I can get the 'counter' value.
Is there a better way (e.g. best practice, fewer lines of code, an easier to understand method, etc.) than this to achieve what I'm trying to do?

The use of GlobalKey is definitely the recommended approach if absolutely you have to access the state of a widget from outside. However, in this case, you shouldn't use either approach.
_SecondClassState should contain the counter, and you should pass it, along with the increaseCounter function, as parameters to FirstClass. If you want to increase the number, just call that function.
Something along these lines:
class SecondClass extends StatefulWidget {
#override
_SecondClassState createState() => _SecondClassState();
}
class _SecondClassState extends State<SecondClass> {
int counter = 0;
//Increases counter by 1
void _increaseCounter() {
setState(() {
counter++;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("MyApp"),
centerTitle: true,
),
body: Column(
children: <Widget>[
FirstClass(counter: counter, increaseCounter: _increaseCounter), //Displays the button to increase 'counter'
FlatButton(
child: Text("Submit"),
onPressed: () {
print(counter.toString()); //I need to get the value of 'counter' here to use elsewhere
}),
],
),
);
}
}
class FirstClass extends StatefulWidget {
final int counter;
final Function increaseCounter;
FirstClass({this.counter, this.increaseCounter});
final _FirstClassState firstClassState = _FirstClassState();
#override
_FirstClassState createState() => firstClassState;
}
class _FirstClassState extends State<FirstClass> {
#override
Widget build(BuildContext context) {
//A button which increases counter when clicked
return FlatButton.icon(
icon: Icon(Icons.add),
label: Text("Counter: ${widget.counter}"),
onPressed: () {
widget.increaseCounter();
},
);
}
}

Related

Stateful widget class variable's value changed while accessing again using widget object

I have experience a weird behaviour in Flutter Stateful widget. I am initializing a local list from the class's final list variable using widget.variable_name. I have incorporated a function to delete an item from the local list but it affects the class variable too. How is this even possible?
Here is the code:
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
const Test({
required this.fruits
});
final List<String> fruits;
static const String routeName = '/test';
static Route route({required List<String> fruits}){
return MaterialPageRoute(
settings: RouteSettings(name: routeName),
builder: (_) => Test(fruits: fruits,)
);
}
#override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
List<String>? _fruits;
void deleteFruit(name){
setState((){
_fruits!.remove(name);
print("_fruits: ${widget.fruits}");
print("widget.fruits: ${widget.fruits}");
});
}
#override
void initState() {
super.initState();
setState(() {
_fruits = widget.fruits;
print("Initial fruits: $_fruits");
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Container(
child: Column(
children:
(_fruits != null) ?
_fruits!.map((e) => ListTile(
leading: IconButton(
icon: Icon(Icons.remove),
onPressed: (){
deleteFruit(e);
}),
title: Text(e),
),
).toList() : []
),
),
);
}
}
I am routing to this stateful widget from another widget using
Navigator.pushNamed(context, '/test',arguments: ["apple","mango","orange"]);
When I delete an item say "mango" from the list, the output is as follows,
_fruits : ["apple","orange"]
widget.fruits : ["apple","orange"] \\I was expecting to get ["apple","mango","orange"]
I suspect the instance of object is passed by reference in your case. Try creating new list like
_fruits = widget.fruits.toList();
Also while feeding on constructor, you can DO
Test(fruits: yourItems.toList(),
You can check Is Dart pass by reference
One should see that Dart it isn't a language that copy the value of variables. So when you pass the fruits down the state, you'll be sending the reference of that list, and that reference points out to the original object. To solve this, you need to copy the list before remove, and assign the new copy to the _fruits variable at the state.
class _TestState extends State<Test> {
List<String>? _fruits;
...
#override
void initState() {
super.initState();
setState(() {
_fruits = [...widget.fruits]; // Copies the objects reference to a new List
print("Initial fruits: $_fruits");
});
}
...

How to pass a GlobalKey through Stateless Widget Children

I'm trying to create a custom menu bar in my app. Right now, the biggest issue I'm having is passing a state for when it's expanded to it's children after a setState occurs.
I thought about inheritance, but from what I've tried all inheritance needs to be in-line. I can't create a widget where the children [] are fed into the constructor on an ad-hoc basis.
My current approach is to use a GlobalKey to update the State of the children widgets being inserted into the StateFul while updating them directly.
The children for my MenuBar are declared as:
List<MenuBarItem> menuItems;
MenuBarItem is an abstract interface class that I intend to use to limit the widgets that can be fed in as menuItems to my MenuBar.
abstract class iMenuItem extends Widget{}
class MenuBarItem extends StatefulWidget implements iMenuItem{
At some iterations of this script, I had a bool isExpanded as part of the iMenuItem, but determined it not necessary.
Here is my code at its current iteration:
My Main:
void main() {
// runApp(MainApp());
//runApp(InherApp());
runApp(MenuBarApp());
}
class MenuBarApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return MaterialApp(
home: Scaffold(
body: MenuBar(
menuItems: [
// This one does NOT work and is where I'm trying to get the
// value to update after a setState
MenuBarItem(
myText: 'Outsider',
),
],
),
),
);
}
}
My Code:
import 'package:flutter/material.dart';
/// Primary widget to be used in the main()
class MenuBar extends StatefulWidget{
List<MenuBarItem> menuItems;
MenuBar({
required this.menuItems,
});
#override
State<MenuBar> createState() => MenuBarState();
}
class MenuBarState extends State<MenuBar>{
bool isExpanded = false;
late GlobalKey<MenuBarContainerState> menuBarContainerStateKey;
#override
void initState() {
super.initState();
menuBarContainerStateKey = GlobalKey();
}
#override
Widget build(BuildContext context){
return MenuBarContainer(
menuItems: widget.menuItems,
);
}
}
class MenuBarContainer extends StatefulWidget{
List<MenuBarItem> menuItems;
late Key key;
MenuBarContainer({
required this.menuItems,
key,
}):super(key: key);
#override
MenuBarContainerState createState() => MenuBarContainerState();
}
class MenuBarContainerState extends State<MenuBarContainer>{
bool isExpanded = false;
#override
void initState() {
super.initState();
isExpanded = false;
}
#override
Widget build(BuildContext context){
List<Widget> myChildren = [
ElevatedButton(
onPressed: (){
setState((){
this.isExpanded = !this.isExpanded;
});
},
child: Text('Push Me'),
),
// This one works. No surprise since it's in-line
MenuBarItem(isExpanded: this.isExpanded, myText: 'Built In'),
];
myChildren.addAll(widget.menuItems);
return Container(
child: Column(
children: myChildren,
),
);
}
}
/// The item that will appear as a child of MenuBar
/// Uses the iMenuItem to limit the children to those sharing
/// the iMenuItem abstract/interface
class MenuBarItem extends StatefulWidget implements iMenuItem{
bool isExpanded;
String myText;
MenuBarItem({
key,
this.isExpanded = false,
required this.myText,
}):super(key: key);
#override
State<MenuBarItem> createState() => MenuBarItemState();
}
class MenuBarItemState extends State<MenuBarItem>{
#override
Widget build(BuildContext context){
GlobalKey<MenuBarState> _menuBarState;
return Row(
children: <Widget> [
Text('Current Status:\t${widget.isExpanded}'),
Text('MenuBarState GlobalKey:\t${GlobalKey<MenuBarState>().currentState?.isExpanded ?? false}'),
Text(widget.myText),
],
);
}
}
/// To give a shared class to any children that might be used by MenuBar
abstract class iMenuItem extends Widget{
}
I've spent 3 days on this, so any help would be appreciated.
Thanks!!
I suggest using ChangeNotifier, ChangeNotifierProvider, Consumer and context.read to manage state. You have to add this package and this import: import 'package:provider/provider.dart';. The steps:
Set up a ChangeNotifier holding isExpanded value, with a setter that notifies listeners:
class MyNotifier with ChangeNotifier {
bool _isExpanded = false;
bool get isExpanded => _isExpanded;
set isExpanded(bool isExpanded) {
_isExpanded = isExpanded;
notifyListeners();
}
}
Insert the above as a ChangeNotifierProvider in your widget tree at MenuBar:
class MenuBarState extends State<MenuBar> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyNotifier(),
child: MenuBarContainer(
menuItems: widget.menuItems,
));
}
}
After this you can easily read and write the isExpanded value from anywhere in your widget tree under the ChangeNotifierProvider, for example:
ElevatedButton(
onPressed: () {
setState(() {
final myNotifier = context.read<MyNotifier>();
myNotifier.isExpanded = !myNotifier.isExpanded;
});
},
child: Text('Push Me'),
),
And if you want to use this state to automatically build something when isExpanded is changed, use Consumer, which will be notified automatically upon every change, for example:
class MenuBarItemState extends State<MenuBarItem> {
#override
Widget build(BuildContext context) {
return Consumer<MyNotifier>(builder: (context, myNotifier, child) {
return Row(
children: <Widget>[
Text('Current Status:\t${myNotifier.isExpanded}'),
Text(widget.myText),
],
);
});
}
}

Best way to use the setState in a method for two classes in Flutter

I have a doubt, i am new in Flutter and i want to know if there is a better way to do what i want, please see the next example of code.
This is my screen and i have to classes, one is for when the device height is less than 800 and the other when is higher
class MyPageExample extends StatelessWidget {
#override
Widget build(BuildContext context) {
var screenSize = MediaQuery.of(context).size;
return Scaffold(
body: SafeArea(
child: screenSize.height > 800
? SignupLarge()
: SignupSmall()
)
);
}
}
This are my two StatefulWidgets
class SignupLarge extends StatefulWidget {
const SignupLarge({ Key? key }) : super(key: key);
#override
_SingupLargeState createState() => _SingupLargeState();
}
class _SingupLargeState extends State<SignupLarge> {
#override
Widget build(BuildContext context) {
return Column(
children: [
// Wome widgets...
ElevatedButton(
onPressed: () => signupToFirebase(),
child: Text('Click me')
)
],
);
}
}
class SignupSmall extends StatefulWidget {
const SignupSmall({ Key? key }) : super(key: key);
#override
_SignupSmallState createState() => _SignupSmallState();
}
class _SignupSmallState extends State<SignupSmall> {
#override
Widget build(BuildContext context) {
return ListView(
children: [
// Wome widgets...
ElevatedButton(
onPressed: () => signupToFirebase(),
child: Text('Click me')
)
],
);
}
}
signupToFirebase(){
// I need to use setState here to show the loading widget
// await Firebase.instance.signupWithEmailAndPassword...
// I need to use setState here to hide the loading widget
}
What i want to do is use the method in my classes using the setState, actually i cant because my method is not inside my classes but if i put my method inside i cant use the same method in both classes.
the above code is just an example of how my code looks like, im tryng to show a loading when a user is signed up.
Someone knows the right way to use the setState for my method using the both classes?
You can give that function another function for it to call.
void signupToFirebase(void Function() whenDone) {
//...
whenDone();
}
...
// caller
signupToFirebase(() {
setState(() {
});
});
...
Or even better, you can have that function return a Future if what you wanted to do is to act once it's done.
Future<void> signupToFirebase() {
await FirebaseAuth.instance... signup() kind of function that returns a Future
}
...
// caller
await signupToFirebase();
setState(() {
});
...

This class is marked as '#immutable', but one or more of its instance fields are not final:

If I am declaring the variables as final then the values(variables) I want to change(on pressed) are in setState(){} so those variables can be changed What to do to prevent this?
Also, why is it written widget.value?
I have tried using static instead of final doesn't work
class BottomCard extends StatefulWidget {
String title;
int value;
#override
_BottomCardState createState() => _BottomCardState(); }
class _BottomCardState extends State<BottomCard> {.....
....<Widget>[
FloatingActionButton(
elevation: 0,
child: Icon(FontAwesomeIcons.plus),
onPressed: () {
setState(() {
widget.value++;
});
},
backgroundColor: Color(0xFF47535E),
),
If you don't need to get variables (title & value) from outside your widget, you can declare them in the _BottomCardState class and use them as you want, for example:
class BottomCardState extends StatefulWidget {
#override
_BottomCardStateState createState() => _BottomCardStateState();
}
class _BottomCardStateState extends State<BottomCardState> {
int _value;
String title;
#override
void initState() {
super.initState();
_value = 0;
title = "any thing";
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
elevation: 0,
child: Icon(FontAwesomeIcons.plus),
onPressed: () {
setState(() {
_value++; // increment value here
});
},
),
);
}
}
If you need to get variables (value & title) from other class, then you need to:
Mark them as final, or
Get them from constructor
To access their values in _BottomCardStateState, you can access them using widget._value. They are final so you can't modify them. For example:
class App extends StatelessWidget {
const App({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: BottomCardState(2,"some thing"),
);
}
}
class BottomCardState extends StatefulWidget {
final int _value;
final String title;
BottomCardState(this._value,this.title)
#override
_BottomCardStateState createState() => _BottomCardStateState();
}
class _BottomCardStateState extends State<BottomCardState> {
int value ;
#override
Widget build(BuildContext context) {
value = widget._value ;
return Scaffold(
floatingActionButton: FloatingActionButton(
elevation: 0,
child: Icon(FontAwesomeIcons.plus),
onPressed: () {
setState(() {
value++; // increment value here
});
},
),
);
}
}
If I am declaring the variables as final then then the values(variables) i want to change(on pressed) are in set state(){} so those variables can be changed What to do to prevent this?
Extend StatefulWidget.
Also why is it written widget.value?
This means you are accessing value in your StatefulWidget subclass and this value is actually defined in your State class.
Full code should look like:
class BottomCardState extends StatefulWidget {
#override
_BottomCardStateState createState() => _BottomCardStateState();
}
class _BottomCardStateState extends State<BottomCardState> {
int _value = 0; // set value here
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
elevation: 0,
child: Icon(FontAwesomeIcons.plus),
onPressed: () {
setState(() {
_value++; // increment value here
});
},
),
);
}
}
Just added this line top of the error class.
//ignore: must_be_immutable

how to keep the state of my widgets after scrolling?

I'm codeing an app with flutter an i'm haveing problems with the development. I'm trying to have a listview with a custom widget that it has a favourite icon that represents that you have liked it product. I pass a boolean on the constructor to set a variables that controls if the icons is full or empty. When i click on it i change it state. It works awesome but when i scroll down and up again it loses the lastest state and returns to the initial state.
Do you know how to keep it states after scrolling?
Ty a lot <3
Here is my code:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index){
return new LikeClass(liked: false);
},
),
);
}
}
class LikeClass extends StatefulWidget {
final bool liked;//i want this variable controls how heart looks like
LikeClass({this.liked});
#override
_LikeClassState createState() => new _LikeClassState();
}
class _LikeClassState extends State<LikeClass> {
bool liked;
#override
void initState() {
liked=widget.liked;
}
#override
Widget build(BuildContext context) {
return new Container(
child: new Column(
children: <Widget>[
new GestureDetector(
onTap:((){
setState(() {
liked=!liked;
//widget.liked=!widget.liked;
});
}),
child: new Icon(Icons.favorite, size: 24.0,
color: liked?Colors.red:Colors.grey,
//color: widget.liked?Colors.red:Colors.grey,//final method to control the appearance
),
),
],
),
);
}
}
You have to store the state (favorite or not) in a parent widget. The ListView.builder widget creates and destroys items on demand, and the state is discarded when the item is destroyed. That means the list items should always be stateless widgets.
Here is an example with interactivity:
class Item {
Item({this.name, this.isFavorite});
String name;
bool isFavorite;
}
class MyList extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyListState();
}
class MyListState extends State<MyList> {
List<Item> items;
#override
void initState() {
super.initState();
// Generate example items
items = List<Item>();
for (int i = 0; i < 100; i++) {
items.add(Item(
name: 'Item $i',
isFavorite: false,
));
}
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListItem(
items[index],
() => onFavoritePressed(index),
);
},
);
}
onFavoritePressed(int index) {
final item = items[index];
setState(() {
item.isFavorite = !item.isFavorite;
});
}
}
class ListItem extends StatelessWidget {
ListItem(this.item, this.onFavoritePressed);
final Item item;
final VoidCallback onFavoritePressed;
#override
Widget build(BuildContext context) {
return ListTile(
title: Text(item.name),
leading: IconButton(
icon: Icon(item.isFavorite ? Icons.favorite : Icons.favorite_border),
onPressed: onFavoritePressed,
),
);
}
}
If you don't have many items in the ListView you can replace it with a SingleChildScrollview and a Column so that the Widgets aren't recycled. But it sounds like you should have a list of items where each item has an isFavourite property, and control the icon based on that property. Don't forget to setState when toggling the favorite.
Other answer are better for your case but this an alternative and can be used if you want to only keep several elements alive during a scroll. In this case you can use AutomaticKeepAliveClientMixin with keepAlive.
class Foo extends StatefulWidget {
#override
FooState createState() {
return new FooState();
}
}
class FooState extends State<Foo> with AutomaticKeepAliveClientMixin {
bool shouldBeKeptAlive = false;
#override
Widget build(BuildContext context) {
super.build(context);
shouldBeKeptAlive = someCondition();
return Container(
);
}
#override
bool get wantKeepAlive => shouldBeKeptAlive;
}
ListView.builder & GridView.builder makes items on demand. That means ,they construct item widgets & destroy them when they going beyond more than cacheExtent.
So you cannot keep any ephemeral state inside that item widgets.(So most of time item widgets are Stateless, but when you need to use keepAlive you use Stateful item widgets.
In this case you have to keep your state in a parent widget.So i think the best option you can use is State management approach for this. (like provider package, or scoped model).
Below link has similar Example i see in flutter.dev
Link for Example
Hope this answer will help for you
A problem with what you are doing is that when you change the liked variable, it exists in the Widget state and nowhere else. ListView items share Widgets so that only a little more than are visible at one time are created no matter how many actual items are in the data.
For a solution, keep a list of items as part of your home page's state that you can populate and refresh with real data. Then each of your LikedClass instances holds a reference to one of the actual list items and manipulates its data. Doing it this way only redraws only the LikedClass when it is tapped instead of the whole ListView.
class MyData {
bool liked = false;
}
class _MyHomePageState extends State<MyHomePage> {
List<MyData> list;
_MyHomePageState() {
// TODO use real data.
list = List<MyData>();
for (var i = 0; i < 100; i++) list.add(MyData());
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new ListView.builder(
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return new LikeClass(list[index]);
},
),
);
}
}
class LikeClass extends StatefulWidget {
final MyData data;
LikeClass(this.data);
#override
_LikeClassState createState() => new _LikeClassState();
}
class _LikeClassState extends State<LikeClass> {
#override
Widget build(BuildContext context) {
return new Container(
child: new Column(
children: <Widget>[
new GestureDetector(
onTap: (() {
setState(() {
widget.data.liked = !widget.data.liked;
});
}),
child: new Icon(
Icons.favorite,
size: 24.0,
color: widget.data.liked ? Colors.red : Colors.grey,
),
),
],
),
);
}
}