Search code examples
flutterflutter-state

Managing mutable state in StatefulWidget / State


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 StatefulWdigets 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:

  1. The state object is created in the createState method of the StatefulWidget. 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 the StatefulWidget 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.

  2. 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 the StatefulWidget to the State object. They suggest always accessing such data via the widget of the State object. But that leads you back to the problem of the StatefulWidget being immutable: you won't be able to replace the list if it is a final field on the StatefulWidget 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(() {});
            },
          )
        );
      }
    }