Сохранение состояния StatefulWidget во время сборки во Flutter

У меня есть список виджетов с отслеживанием состояния, где пользователь может добавлять, удалять и взаимодействовать с элементами в списке. Удаление элементов из списка приводит к тому, что последующие элементы в списке перестраиваются по мере их сдвига, чтобы заполнить удаленную строку. Это приводит к потере данных состояния для этих виджетов, хотя они должны оставаться неизменными, кроме их местоположения на экране. Я хочу иметь возможность сохранять состояние для остальных элементов в списке даже при изменении их положения.

Ниже представлена ​​упрощенная версия моего приложения, которая состоит в основном из списка StatefulWidgets. Пользователь может добавлять элементы в список («задачи» в моем приложении) с помощью кнопки с плавающим действием или удалять их, проводя пальцем по экрану. Любой элемент в списке можно выделить, нажав на него, что изменит состояние цвета фона элемента. Если в списке выделено несколько элементов, и элемент (кроме последнего элемента в списке) удален, элементы, которые перемещаются для замены удаленного элемента, теряют данные своего состояния (т. Е. Цвет фона сбрасывается на прозрачный). Я подозреваю, что это потому, что _taskList перестраивается, поскольку я вызываю setState () для обновления отображения после удаления задачи. Я хочу знать, есть ли чистый способ сохранить данные о состоянии для оставшихся задач после удаления задачи из _taskList.

void main() => runApp(new TimeTrackApp());

class TimeTrackApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Time Tracker',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new TimeTrackHome(title: 'Task List'),
    );
  }
}

class TimeTrackHome extends StatefulWidget {
  TimeTrackHome({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _TimeTrackHomeState createState() => new _TimeTrackHomeState();
}

class _TimeTrackHomeState extends State<TimeTrackHome> {
  TextEditingController _textController;
  List<TaskItem> _taskList = new List<TaskItem>();

  void _addTaskDialog() async {
    _textController = TextEditingController();

    await showDialog(
        context: context,
        builder: (_) => new AlertDialog(
              title: new Text("Add A New Task"),
              content: new TextField(
                controller: _textController,
                decoration: InputDecoration(
                    border: InputBorder.none, hintText: 'Enter the task name'),
              ),
              actions: <Widget>[
                new FlatButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text("CANCEL")),
                new FlatButton(
                    onPressed: (() {
                      Navigator.pop(context);
                      _addTask(_textController.text);
                    }),
                    child: const Text("ADD"))
              ],
            ));
  }

  void _addTask(String title) {
    setState(() {
      // add the new task
      _taskList.add(TaskItem(
        name: title,
      ));
    });
  }

  @override
  void initState() {
    _taskList = List<TaskItem>();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Align(
        alignment: Alignment.topCenter,
        child: ListView.builder(
            padding: EdgeInsets.all(0.0),
            itemExtent: 60.0,
            itemCount: _taskList.length,
            itemBuilder: (BuildContext context, int index) {
              if (index < _taskList.length) {
                return Dismissible(
                  key: ObjectKey(_taskList[index]),
                  onDismissed: (direction) {
                    if(this.mounted) {
                      setState(() {
                        _taskList.removeAt(index);
                      });
                    }
                  },
                  child: _taskList[index],
                );
              }
            }),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _addTaskDialog,
        tooltip: 'Click to add a new task',
        child: new Icon(Icons.add),
      ),
    );
  }
}

class TaskItem extends StatefulWidget {
  final String name;

  TaskItem({Key key, this.name}) : super(key: key);
  TaskItem.from(TaskItem other) : name = other.name;

  @override
  State<StatefulWidget> createState() => new _TaskState();
}

class _TaskState extends State<TaskItem> {
  static final _taskFont =
  const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
  Color _color = Colors.transparent;

  void _highlightTask() {
    setState(() {
      if(_color == Colors.transparent) {
        _color = Colors.greenAccent;
      }
      else {
        _color = Colors.transparent;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Material(
        color: _color,
        child: ListTile(
          title: Text(
            widget.name,
            style: _taskFont,
            textAlign: TextAlign.center,
          ),
          onTap: () {
            _highlightTask();
          },
        ),
      ),
      Divider(
        height: 0.0,
      ),
    ]);
  }
}

person Ben    schedule 13.07.2018    source источник


Ответы (1)


В итоге я решил проблему, создав промежуточный класс, который содержит ссылку на StatefulWidget и передается по всем переменным состояния. Класс State получает доступ к переменным состояния через ссылку на промежуточный класс. Виджет более высокого уровня, который содержал и управлял списком StatefulWidget, теперь обращается к StatefulWidget через этот промежуточный класс. Я не совсем уверен в «правильности» своего решения, поскольку я не нашел других примеров этого, поэтому я все еще открыт для предложений.

Мой промежуточный класс выглядит следующим образом:

class TaskItemData {
  // StatefulWidget reference
  TaskItem widget;
  Color _color = Colors.transparent;

  TaskItemData({String name: "",}) {
    _color = Colors.transparent;
    widget = TaskItem(name: name, stateData: this,);
  }
}

Мой StatefulWidget и соответствующие ему классы State практически не изменились, за исключением того, что переменные состояния больше не находятся в классе State. Я также добавил ссылку на промежуточный класс внутри моего StatefulWidget, который инициализируется в конструкторе. Предыдущее использование переменных состояния в моем классе State теперь доступно через ссылку на промежуточный класс. Модифицированные классы StatefulWidget и State выглядят следующим образом:

class TaskItem extends StatefulWidget {
  final String name;
  // intermediate class reference
  final TaskItemData stateData;

  TaskItem({Key key, this.name, this.stateData}) : super(key: key);

  @override
  State<StatefulWidget> createState() => new _TaskItemState();
}

class _TaskItemState extends State<TaskItem> {
  static final _taskFont =
  const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);

  void _highlightTask() {
    setState(() {
      if(widget.stateData._color == Colors.transparent) {
        widget.stateData._color = Colors.greenAccent;
      }
      else {
        widget.stateData._color = Colors.transparent;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(children: <Widget>[
      Material(
        color: widget.stateData._color,
        child: ListTile(
          title: Text(
            widget.name,
            style: _taskFont,
            textAlign: TextAlign.center,
          ),
          onTap: () {
            _highlightTask();
          },
        ),
      ),
      Divider(
        height: 0.0,
      ),
    ]);
  }
}

Виджет, содержащий список объектов TaskItem, был заменен списком TaskItemData. Дочерний элемент ListViewBuilder теперь обращается к виджету TaskItem через промежуточный класс (то есть дочерний элемент: _taskList [index], был изменен на дочерний элемент: _taskList [index] .widget,). Это выглядит следующим образом:

class _TimeTrackHomeState extends State<TimeTrackHome> {
  TextEditingController _textController;
  List<TaskItemData> _taskList = new List<TaskItemData>();

  void _addTaskDialog() async {
    _textController = TextEditingController();

    await showDialog(
        context: context,
        builder: (_) => new AlertDialog(
          title: new Text("Add A New Task"),
          content: new TextField(
            controller: _textController,
            decoration: InputDecoration(
                border: InputBorder.none, hintText: 'Enter the task name'),
          ),
          actions: <Widget>[
            new FlatButton(
                onPressed: () => Navigator.pop(context),
                child: const Text("CANCEL")),
            new FlatButton(
                onPressed: (() {
                  Navigator.pop(context);
                  _addTask(_textController.text);
                }),
                child: const Text("ADD"))
          ],
        ));
  }

  void _addTask(String title) {
    setState(() {
      // add the new task
      _taskList.add(TaskItemData(
        name: title,
      ));
    });
  }

  @override
  void initState() {
    _taskList = List<TaskItemData>();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Align(
        alignment: Alignment.topCenter,
        child: ListView.builder(
            padding: EdgeInsets.all(0.0),
            itemExtent: 60.0,
            itemCount: _taskList.length,
            itemBuilder: (BuildContext context, int index) {
              if (index < _taskList.length) {
                return Dismissible(
                  key: ObjectKey(_taskList[index]),
                  onDismissed: (direction) {
                    if(this.mounted) {
                      setState(() {
                        _taskList.removeAt(index);
                      });
                    }
                  },
                  child: _taskList[index].widget,
                );
              }
            }),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _addTaskDialog,
        tooltip: 'Click to add a new task',
        child: new Icon(Icons.add),
      ),
    );
  }
}
person Ben    schedule 19.07.2018
comment
Классный подход! Я работал над аналогичной потребностью, с дополнительной трудностью, связанной с необходимостью начинать с начального списка отклоняемых, иметь возможность добавлять виджеты и отслеживать любые изменения языка, и в этом случае должны быть перестроен! Спасибо за аккуратное решение - person WhoWhenWhat Explorer; 08.05.2019