Вот несколько неловкое предположение, которое я сделал относительно setState, когда начал изучать Flutter почти 4 года назад.

Все мы знаем setState из примера со счетчиком:

class _MyWidgetState extends State<MyWidget> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter = _counter + 1;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text("The current value is $_counter!");
  }
}

Вот, как я думал, это работало:

  1. Есть одно stateful поле под названием _counter.

  2. У нас также есть виджет Text, который отображает значение _counter.

  3. Каждый раз, когда мы хотим обновить _counter, мы также должны обернуть его в анонимную функцию, которую нужно передать в setState. В противном случае фреймворк не будет знать, что было обновлено.

Через четыре месяца моего путешествия по Flutter я обнаружил, что предположение номер три не соответствует действительности.

Мы действительно должны вызывать setState при обновлении _counter, но нет абсолютно никакой необходимости делать это в анонимном обратном вызове.

Это:

setState(() {
  _counter = _counter + 1;
});

ровно то же самое, что и:

_counter = _counter + 1;
setState(() {});

Фреймворк Flutter не проверяет магическим образом код внутри анонимной функции и не выполняет некоторые сравнения, чтобы увидеть, что изменилось.

Каждый раз, когда вы вызываете setState, виджет перестраивается – даже если ничего не изменилось и обратный вызов, переданный в setState, был пуст.

То, что я изначально предполагал о setState, было полностью выдумано в моей голове.

Заглянуть за кулисы

Давайте посмотрим, как выглядит код, лежащий в основе setState:

@protected
void setState(VoidCallback fn) {
  assert(fn != null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
      throw FlutterError.fromParts([/* ... */]);
    }
    if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
      throw FlutterError.fromParts([/* ... */]);
    }
    return true;
  }());
  final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      throw FlutterError.fromParts([/* ... */]);
    }
    return true;
  }());
  _element.markNeedsBuild();
}

Давайте уберем утверждения:

@protected
void setState(VoidCallback fn) {
  final dynamic result = fn() as dynamic;
  _element.markNeedsBuild();
}

На самом деле мы можем свести все к минимуму:

@protected
void setState(VoidCallback fn) {
  fn();
  _element.markNeedsBuild();
}

Всё, что делает setState – это вызывает предоставленный обратный вызов, после чего связанный элемент становится запланированным для новой сборки.

Здесь нет магической диффузии полей.

Так зачем же тогда нам setState?

Благодаря исследованиям UX, проведенным командой Flutter.

Они делают очень много.

Еще в 2017 и 2018 годах на GitHub существовал ярлык проблемы под названием "первый час". Команда Flutter провела UX-исследования для разработчиков и проследила, как новички будут использовать Flutter, если оставить их на час одних. Эти исследования будут определять будущие решения по API для Flutter.

Очевидно, что функция setState является одной из таких находок.

Из статьи на GitHub, которая помогла мне понять, что я жил во лжи:

Раньше у нас был просто метод markNeedsBuild, но мы обнаружили, что люди вызывают его как талисман на удачу – в любой момент, когда они не были уверены, нужно ли вызывать этот метод, они вызывали его.

Мы перешли на метод, который принимает (синхронно вызываемый) обратный вызов, и внезапно у людей стало гораздо меньше проблем с этим.

Есть также ответ на StackOverflow от Collin Jackson из команды Flutter:

Когда во Flutter была функция "markNeedsBuild", разработчики в итоге просто вызывали ее в произвольное время. Когда синтаксис изменился на setState((){ }), разработчики стали гораздо чаще использовать API правильно.

Таким образом, весь API setState – это просто умственный трюк. И, похоже, это работает – очевидно, это привело к тому, что люди стали реже перестраивать свои виджеты.

У меня никогда не было проблем с пониманием того, когда вызывать setState, но я определенно вижу, что задаюсь вопросом "кто такой Марк и что он строит?".

Тайна раскрыта.

Заключение

Что всё это значит?

Должны ли мы теперь преобразовать все наши вызовы setState в нечто подобное?

_counter = _counter + 1;
setState(() {});

Скорее всего, нет.

Несмотря на то, что нет никакой разницы, вызов setState с анонимным обратным вызовом кажется правильным.

На данный момент анонимный обратный вызов является устоявшейся традицией, и все остальное кажется странным.


Материал переведён Ruble.

Комментарии (2)


  1. ookami_kb
    04.07.2023 08:29
    +4

    На данный момент анонимный обратный вызов является устоявшейся традицией, и все остальное кажется странным.

    Колбэк несет в себе полезную информацию другим разработчикам: становится гораздо понятнее, зачем именно здесь нужен setState().

    Как раз, чтобы не гадать, был ли он вызван "как талисман на удачу", или его забыли удалить после рефакторинга, или он действительно делает что-то полезное, помещайте туда причину вызова, такой формат сразу всё объясняет:

    setState(() {
      _value = newValue;
    });

    Поэтому в документации и пишут:

    Generally it is recommended that the setState method only be used to wrap the actual changes to the state, not any computation that might be associated with the change.

    И поэтому в DCM есть правило в тему.


  1. paamayim
    04.07.2023 08:29

    Это только вершина айсберга. Еще месяца через 4 вы возможно поймете как устроен жизненый цикл виджетов, узнаете что на самом деле представляет из себя контекст и может даже погрузитесь в то как связаны между собой виджеты, элементы и рендеробжекты. Поиграетесь с инхерит виджетами, стримами и изолятами. А через год другой будете ломать голову над тем какие архитектуры использовать в том или ином проекте и может даже напишите свой кастомный роутер на навигаторе 2.0). Ну по крайней мере у меня так..