I've been slowly building an app with Flutter and am struggling to work under the StatefulWidget/State paradigm.
I am used to being able to read or alter the state of some UI component from other classes and do not quite understand how to do this in Flutter. For example, right now I am working to build a ListView that contains CheckBoxListTiles. I would like to, from another class, go through and read the state of each of the checkboxes in the list tiles.
I feel like this should be very straightforward in practice but I an unable to come up with a means or an example to effectively externalize the state of a given Widget such that it can be worked with elsewhere. I am aware that there may very well be an intentional and philosophical reason behind the apparent difficulty and can myself see reasons why it would be critical in a framework such as Flutter. However, I feel restricted in that it is not obvious to a newcomer how to do work under the paradigm off the bat.
How can I go about reading the values of the checkboxes from the ItemsList class?
The Item class:
class ListItem extends StatefulWidget {
String _title = "";
bool _isSelected = false;
ListItem(this._title);
@override
State<StatefulWidget> createState() => new ListItemState(_title);
}
class ListItemState extends State<ListItem> {
String _title = "";
bool _isSelected = false;
ListItemState(this._title);
@override
Widget build(BuildContext context) {
return new CheckboxListTile(
title: new Text(_title),
value: _isSelected,
onChanged: (bool value) {
setState(() {
_isSelected = value;
});
},
);
}
String getTitle() {
return _title;
}
bool isSelected() {
return _isSelected;
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
}
And the list class:
class ItemsList extends StatefulWidget {
@override
State<StatefulWidget> createState() => new ItemsListState();
}
class ItemsListState extends State<ItemsList> {
List<ListItem> _items = new List();
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView(
children: _items.map((ListItem item) {
return item;
}).toList(),
),
floatingActionButton: new FloatingActionButton(
onPressed: addItem,
child: new Icon(Icons.add),),
);
}
void addItem() {
setState(() {
_items.add(new ListItem("Item"));
});
}
List<String> getSelectedTitles() {
List<String> selected = new List();
***This is where I would like to externalize the state***
for(ListItem e in _items) {
if(e.isSelected()) {
selected.add(e.getTitle());
}
}
return selected;
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
}
A stateful widget is a widget that describes part of the user interface by building a constellation of other widgets that describe the user interface more concretely.
Stateful Widgets: The widgets whose state can be altered once they are built are called stateful Widgets. These states are mutable and can be changed multiple times in their lifetime. This simply means the state of an app can change multiple times with different sets of variables, inputs, data.
State is information that (1) can be read synchronously when the widget is built and (2) might change during the lifetime of the widget. It is the responsibility of the widget implementer to ensure that the State is promptly notified when such state changes, using State.
The lifecycle of a stateful Widget explains the calling hierarchy of functions and changes in the state of the widget. Everything in the flutter is a widget. Basically, the Stateful and stateless are the types of widget in the flutter.
Flutter is different from your usual app. It uses principles similar to react, which themselves are similar to functional programming.
Which means flutters comes with a set of restrictions that forces you to architecture your app differently :
And as reward, you can be almost certain that any event don't have implicit unknown consequences on another unrelated class.
Okey, but what does this change to my example ?
That's pretty straightforward : It means that it's impossible for say a List, to access informations from one of the list item.
What you should do instead is the opposite : Store the information of all list items inside the list. And pass it down to each.
So first we need to change ListItem
so that it gets everything from it's parent. We can also make it stateless because it doesn't store any information anymore.
With these, ListItem
suddenly becomes pretty straightforward :
class ListItem extends StatelessWidget {
final String title;
final bool isSelected;
final ValueChanged<bool> onChange;
ListItem({ @required this.onChange, this.title = "", this.isSelected = false, });
@override
Widget build(BuildContext context) {
return new CheckboxListTile(
title: new Text(title),
value: isSelected,
onChanged: onChange
);
}
}
Notice that all fields are immutable. And that the onChange
event passed to CheckboxListTile
is passed as parameter too !
At this point you may think "But isn't that class pointless now" ? Not really. In our case the layout is dead simple. But this is a good practice to create such classes, as it splits layout. Cleaning the code of the parent for layout logic.
Anyway let's continue.
Now, let's modify the parent so that it contains informations and pass it down :
class ItemsList extends StatefulWidget {
State<StatefulWidget> createState() => new ItemsListState();
}
class ItemsListState extends State<ItemsList> {
List<ListItem> items = new List<ListItem>();
@override
Widget build(BuildContext context) {
return new Scaffold(
body: new ListView(
children: items,
),
floatingActionButton: new FloatingActionButton(
onPressed: addItem,
child: new Icon(Icons.add),
),
);
}
void addItem() {
final listItemIndex = items.length;
final newList = new List<ListItem>.from(items)
..add(
new ListItem(
onChange: (checked) => onListItemChange(listItemIndex, checked),
title: "Item n' $listItemIndex",
isSelected: false,
),
);
this.setState(() {
items = newList;
});
}
onListItemChange(int listItemIndex, bool checked) {
final newList = new List<ListItem>.from(items);
final currentItem = items[listItemIndex];
newList[listItemIndex] = new ListItem(
onChange: currentItem.onChange,
isSelected: checked,
title: currentItem.title,
);
this.setState(() {
items = newList;
});
}
}
Notice here how instead of mutating the items
list, I create a new one everytimes.
Why is it ? That is because, like I explained before, fields are supposed to be immutable. ItemsList
may be stateful, but it's not a reason to not follow that principle. So instead of editing the list, we create a new one based on the old one.
Fact is that without that new list, ListView
would think "Hey, you sent me the same list as before. So I don't need to refresh right ?".
The cool thing now, is that by default ItemsList
has all the informations about ListItem
. So getSelectedTitles
becomes pretty easy to do :
Iterable<String> getSelectedTitles() {
return items.where((item) => item.isSelected).map((item) => item.title);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With