Issue
I'm a bit confused about how to manage state withing a StatefulWidget. The following is a simplified example of a problem I am running into:
I have a StatefulWidget
designed to display a list of things. It is constructed with a list that was retrieved from an API. The widget also allows the user to add new items to the list, hitting the API to do so. The API in turn returns the entire list after adding the item.
The challenge I'm running into is that the widget is constructed with an api.List
object. Dart requires StatefulWdiget
s to be immutable (and therefore all of the fields final), and therefore the list cannot be replaced with a new list returned by the API when a user adds an item to it. That part makes sense to me: mutable state should live in the state object, not the widget. So that would suggest that the state object should keep track of the list. That leads to two different problems:
The state object is created in the
createState
method of theStatefulWidget
. If we wanted to pass in the list to the constructor of the state object, the list would need to be stored as a final field on theStatefulWidget
as well. Now both the widget and it's state object would have a reference to the list. As the user adds items to the list, and the state object replaces its list with the new copy returned from the API, the two would be out of sync.Even if you did this, the linter will complain about including logic in the
createState
method: https://dart-lang.github.io/linter/lints/no_logic_in_create_state.html. It appears that they strongly suggest state objects should always have 0-argument constructors and you should never passing anything from theStatefulWidget
to theState
object. They suggest always accessing such data via thewidget
of theState
object. But that leads you back to the problem of theStatefulWidget
being immutable: you won't be able to replace the list if it is a final field on theStatefulWidget
object.
Problematic design 1
Here, we avoid the linting issue of having logic in the createState
method. But you cannot replace widget.list
with newList
since it is final.
class ListWidget extends StatefulWidget {
const ListWidget({Key? key, required this.list }) : super(key: key);
final api.List list;
@override
State<StatefulWidget> createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
... // bunch of widgets
ElevatedButton(
...
onPressed: () async {
final newList = await api.addNewListItem(...);
// Can't do this because widget.list is final. Makes sense as it just seems wrong to store mutable state in the widget instead of the state.
setState(() => widget.list = newList);
},
)
);
}
}
This design could be made to work if instead of replacing the widget.list
object you "internally" mutated all of its fields to match newList
. That seems awkward and error-prone. Also, you still end up mutating (internally) widget.list
, and that feels very wrong. It really seems like flutter's intention is that mutable state really shouldn't live in the StatefulWidget
.
Problematic design 2
This works, but the linter complains about passing the list to the _ListWidgetState
constructor in createState
. Also, as the user adds items to the list, the widget's list
object and the state's list
object become out of sync.
class ListWidget extends StatefulWidget {
const ListWidget({Key? key, required this.list }) : super(key: key);
final api.List list;
@override
State<StatefulWidget> createState() => _ListWidgetState(list); // linter complains
}
class _ListWidgetState extends State<ListWidget> {
_ListWidgetState(this.list);
api.List list;
@override
Widget build(BuildContext context) {
return Scaffold(
... // bunch of widgets
ElevatedButton(
...
onPressed: () async {
final newList = await api.addNewListItem(...);
// This works, but now widget.list and list are out of sync.
setState(() => list = newList);
},
)
);
}
}
Perils of Flutter state in StatefulWidget rather than State? seems to be addressing a similar problem. The suggestion there is to create a new instance of the widget. I'm not sure how you would fully replace the ListWidget
with a newly constructed one with newList
from inside the onPressed
callback though.
Solution
The second snippet is closer the solution. Essentially, nothing makes it necessary for the list
in ListWidget
and the list
in _ListWidgetState
to be in "sync". In fact, the best way to look at it is that the list in the ListWidget
is the initial data and the list in _ListWidgetState
is the updated data, if there is any change made by the user. You can rely upon the data in the second list
as you maintain it when the user makes changes.
import 'package:flutter/material.dart';
class ListWidget extends StatefulWidget {
const ListWidget({Key? key, required this.list }) : super(key: key);
final api.List list;
@override
State<StatefulWidget> createState() => _ListWidgetState();
}
class _ListWidgetState extends State<ListWidget> {
_ListWidgetState();
late api.List list;
@override
void initState() {
list = widget.list;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
...
ElevatedButton(
...
onPressed: () async {
list = await api.addNewListItem(...);
setState(() {});
},
)
);
}
}
Answered By - Aldrin Mathew
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.