Flutter: Как правильно использовать унаследованный виджет?

Каков правильный способ использования InheritedWidget? До сих пор я понял, что он дает вам возможность распространять данные по дереву виджета. В крайнем случае, если вы ставите RootWidget, он будет доступен из всех виджетов в дереве на всех Маршрутах, что отлично, потому что мне нужно сделать мою модель ViewModel/Model доступной для моих виджетов без необходимости прибегать к глобальным или синглтонам.

НО InheritedWidget неизменен, так как я могу его обновить? И что еще важнее, как мои устаревшие виджеты вызвали восстановление своих поддеревьев?

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

Я добавляю цитату из Брайана Игана:

Да, я рассматриваю это как способ распространения данных по дереву. То, что я сбиваю с толку, из документов API:

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

Когда я впервые прочитал это, я подумал:

Я мог бы набить некоторые данные в InheritedWidget и позже мутировать их. Когда эта мутация произойдет, она перестроит все Виджеты, которые ссылаются на мой InheritedWidget. Что я нашел:

Чтобы мутировать состояние InheritedWidget, вам нужно обернуть его в StatefulWidget. Затем вы фактически изменяете состояние StatefulWidget и передаете эти данные на InheritedWidget, который передает данные всем дочерним элементам. Тем не менее, в этом случае, кажется, перестроить все дерево под StatefulWidget, а не только виджеты, ссылающиеся на InheritedWidget. Это верно? Или он каким-то образом знает, как пропустить виджеты, ссылающиеся на InheritedWidget, если updateShouldNotify возвращает false?

Ответ 1

Проблема исходит из вашей цитаты, которая неверна.

Как вы сказали, InheritedWidgets, как и другие виджеты, являются неизменяемыми. Поэтому они не обновляются. Они созданы заново.

Дело в том, что InheritedWidget - это простой виджет, который ничего не делает, кроме хранения данных. У него нет никакой логики обновления или чего-то еще. Но, как и любые другие виджеты, он связан с Element. И угадай что? Эта вещь изменчива, и флаттер будет использовать ее всякий раз, когда это возможно!

Исправленная цитата будет:

InheritedWidget, если на него ссылаются таким образом, заставит потребителя перестраивать при изменении InheritedWidget, связанного с InheritedElement.

Там много говорят о том, как виджеты/элементы/renderbox объединены вместе. Но вкратце, они похожи на это (слева - типичный виджет, в середине - "элементы", а справа - "рендер-боксы"):

enter image description here

Дело в том, что: когда вы создаете новый виджет; флаттер сравнит его со старым. Повторно используйте его "Элемент", который указывает на RenderBox. И измените свойства renderbox.


Хорошо, но как это ответит на мой вопрос?

Ну, это легко. При создании экземпляра InheritedWidget и последующем вызове context.inheritedWidgetOfExactType (или MyClass.of который в основном совпадает); подразумевается, что он будет прослушивать Element связанный с вашим InheritedWidget. И всякий раз, когда этот Element получает новый виджет, он будет принудительно обновлять любые виджеты, которые вызвали предыдущий метод.

Короче говоря, когда вы заменяете существующий InheritedWidget на новый; флаттер увидит, что это изменилось. И будет уведомлять связанные виджеты о потенциальной модификации.

Если вы все поняли, вы должны были уже угадать решение:

Оберните ваш InheritedWidget внутри StatefulWidget который будет создавать новый InheritedWidget всякий раз, когда что-то меняется! В этой ситуации рекомендуется, чтобы ваши данные InheritedWidget фактически являлись экземпляром вашего StatefulWidget а затем делали InheritedWidget частным. Чтобы избежать ненужной копировальной пасты и возможных ошибок.

Конечный результат в реальном коде будет:

class MyInherited extends StatefulWidget {
  Widget child;

  MyInherited({this.child});

  @override
  MyInheritedState createState() => new MyInheritedState();

  static MyInheritedState of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }
}

class MyInheritedState extends State<MyInherited> {
  String _myField;
  // only expose a getter to prevent bad usage
  String get myField => _myField;

  void onMyFieldChange(String newValue) {
    setState(() {
      _myField = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new _MyInherited(
      data: this,
      child: widget.child,
    );
  }
}

/// Only has MyInheritedState as field.
class _MyInherited extends InheritedWidget {
  final MyInheritedState data;

  _MyInherited({Key key, this.data, Widget child}) : super(key: key, child: child);

  @override
  bool updateShouldNotify(_MyInherited old) {
    return true;
  }
}

Но разве создание нового InheritedWidget не перестроит все дерево?

Нет, это не обязательно. Поскольку у вашего нового InheritedWidget потенциально может быть тот же самый дочерний элемент, что и раньше. И под точным я имею в виду тот же пример. Виджеты, которые имеют тот же экземпляр, что и раньше, не перестраиваются.

И в большинстве случаев (имея унаследованный виджет в корне вашего приложения) унаследованный виджет является постоянным. Так что нет необходимости перестраивать.

Ответ 2

TL; DR

Не используйте тяжелые вычисления внутри метода updateShouldNotify и использование прода вместо нового при создании виджета


Прежде всего, мы должны понять, что такое объекты Widget, Element и Render.

  1. Объекты рендеринга - это то, что фактически отображается на экране. Они изменчивы, содержат логику рисования и верстки. Дерево визуализации очень похоже на объектную модель документа (DOM) в Интернете, и вы можете рассматривать объект визуализации как узел DOM в этом дереве.
  2. Виджет - это описание того, что должно быть отображено. Они неизменны и дешевы. Таким образом, если виджет отвечает на вопрос "Что?" (Декларативный подход), тогда объект Render отвечает на вопрос "Как?" (Императивный подход). Аналогия из Интернета - "Виртуальный ДОМ".
  3. Element/BuildContext - это прокси между объектами Widget и Render. Он содержит информацию о положении виджета в дереве * и о том, как обновить объект Render при изменении соответствующего виджета.

Теперь мы готовы окунуться в InheritedWidget и метод BuildContext inheritFromWidgetOfExactType.

В качестве примера я рекомендую рассмотреть этот пример из документации Flutter об InheritedWidget:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  })  : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }

  @override
  bool updateShouldNotify(FrogColor old) {
    return color != old.color;
  }
}

InheritedWidget - просто виджет, который реализует в нашем случае один важный метод - updateShouldNotify. updateShouldNotify - функция, которая принимает один параметр oldWidget и возвращает логическое значение: true или false.

Как и любой виджет, InheritedWidget имеет соответствующий объект Element. Это унаследованный элемент. InheritedElement вызывает updateShouldNotify для виджета каждый раз, когда мы создаем новый виджет (вызов setState для предка). Когда updateShouldNotify возвращает true, InheritedElement перебирает зависимости (?) И вызывает для него метод didChangeDependencies.

Где InheritedElement получает зависимости? Здесь мы должны посмотреть на метод attributeitFromWidgetOfExactType.

inheritFromWidgetOfExactType - этот метод определен в BuildContext, и каждый элемент реализует интерфейс BuildContext (Element == BuildContext). Таким образом, каждый элемент имеет этот метод.

Давайте посмотрим на код наследованияFromWidgetOfExactType:

final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
  assert(ancestor is InheritedElement);
  return inheritFromElement(ancestor, aspect: aspect);
}

Здесь мы пытаемся найти предка в _inheritedWidgets, сопоставленных по типу. Если предок найден, мы тогда вызываем attribute_FromElement.

Код для inheritFromElement:

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. Мы добавляем предка как зависимость текущего элемента (_dependencies.add(ancestor))
  2. Мы добавляем текущий элемент в зависимости от предков (ancestor.updateDependencies(this, аспект))
  3. Мы возвращаем виджет-предок в качестве результата метода inheritFromWidgetOfExactType (возвращаем ancestor.widget)

Итак, теперь мы знаем, где InheritedElement получает свои зависимости.

Теперь давайте посмотрим на метод didChangeDependencies. Каждый элемент имеет этот метод:

  void didChangeDependencies() {
    assert(_active); // otherwise markNeedsBuild is a no-op
    assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
    markNeedsBuild();
  }

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

Но как насчет "Перестроения всего поддерева, когда я перестраиваю InheritedWidget?". Здесь мы должны помнить, что виджеты являются неизменными, и если вы создадите новый виджет, Flutter перестроит поддерево. Как мы можем это исправить?

  1. Кешировать виджеты руками (вручную)
  2. Используйте const, потому что const создает единственный экземпляр значения/класса