'StatefulWidgets in ReorderableListView don't keep their state when reordering the list

Here I have a trimmed down page which creates a ReorderableListView, which has its body set to two RecipeStepWidgets with UniqueKeys set (I've also tried this with ValueKey and ObjectKey)

import 'package:flutter/material.dart';
import 'consts.dart';
import 'recipeStep.dart';
import 'recipeStepWidget.dart';

class NewRecipePage extends StatefulWidget {
  @override
  _NewRecipePageState createState() => _NewRecipePageState();
}

class _NewRecipePageState extends State<NewRecipePage> {
  final TextEditingController _nameController = TextEditingController();
  @override
  Widget build(BuildContext context) {
    List<RecipeStep> recipeSteps = [];

    List<RecipeStepWidget> stepWidgets = [
      RecipeStepWidget(key: UniqueKey()),
      RecipeStepWidget(key: UniqueKey())
    ];

    void _onReorder(int oldIndex, int newIndex) {
      setState(
        () {
          if (newIndex > oldIndex) {
            newIndex -= 1;
          }

          final RecipeStepWidget item = stepWidgets.removeAt(oldIndex);
          stepWidgets.insert(newIndex, item);
        },
      );
    }

    return Scaffold(
      appBar: AppBar(title: Text("New Recipe")),
      body: Column(
        children: <Widget>[
          Expanded(
            child: ReorderableListView(
              header: Text("Steps"),
              onReorder: _onReorder,
              children: stepWidgets,
            ),
          ),
        ],
      ),
    );
  }
}

The RecipeStepWidget class is (ignoring includes):

class RecipeStepWidget extends StatefulWidget {

  RecipeStepWidget({@required Key key}) : super(key: key);

  _RecipeStepWidgetState createState() => _RecipeStepWidgetState();

}

class _RecipeStepWidgetState extends State<RecipeStepWidget> {
  TextEditingController _controller = TextEditingController();
  TextEditingController _durationLowController = TextEditingController();
  TextEditingController _durationHighController = TextEditingController();
  bool concurrent = false;

  RecipeStep toRecipeStep() {
    return RecipeStep(
      text: _controller.text,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        TextField(
          controller: _controller,
          decoration: InputDecoration(hintText: "Step text"),
        ),
        Row(
          children: <Widget>[
            Text("Duration min: "),
            Container(
              width: 40.0,
              //height: 100.0,
              child: TextField(
                controller: _durationLowController,
                keyboardType: TextInputType.number,
                decoration: InputDecoration(hintText: "0"),
                onChanged: (String val) {
                  if (_durationHighController.text.isEmpty ||
                      int.parse(val) >
                          int.parse(_durationHighController.text)) {
                    _durationHighController.text = val;
                  }
                },
              ),
            ),
            Text(" max: "),
            Container(
              width: 40.0,
              //height: 100.0,
              child: TextField(
                controller: _durationHighController,
                keyboardType: TextInputType.number,
                decoration: InputDecoration(hintText: "0"),
              ),
            ),
          ],
        ),
        Row(
          children: <Widget>[
            Text("Start concurrently with previous step"),
            Checkbox(
                value: concurrent,
                onChanged: (bool val) => {
                      setState(() {
                        concurrent = val;
                      })
                    }),
          ],
        ),
      ],
    );
  }
}

When I edit the textfields or checkboxes in the RecipeStateWidgets and then reorder them within the list by clicking+dragging them, the widgets get reset to their default state.

Does anyone have any ideas why this is happening? I thought that all I had to do in order to get the ReorderableListView to work as intended was to set a key on each of its children. Is that not correct?

Thanks!



Solution 1:[1]

I think you can use AutomaticKeepAliveClientMixin like so:

class _RecipeStepWidgetState extends State<RecipeStepWidget> with AutomaticKeepAliveClientMixin  {

  @override
  bool get wantKeepAlive => true;

Solution 2:[2]

Giving the list item a ValueKey seems to fix the problem for me. Hopefully helps in your situation as well.

e.g.

  List<YourModel> _items = [];

  Widget _itemsListWidget() {
    return ReorderableListView(
      onReorder: (oldIndex, newIndex) {
        //
      },
      children: [
        for (int index = 0; index < _items.length; index += 1)
          Text(
            _items[index],
            key: ValueKey(_items[index].id), // <--- This is what you need to add
          ),
      ],
    );
  }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 InsolentWorm
Solution 2 Chris