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

103

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

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

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

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

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

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

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

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

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

Томас
источник

Ответы:

108

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

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

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

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

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

Есть отличный разговор о том, как соединяются вместе виджеты / элементы / рендербокс . Но вкратце они такие (слева - ваш типичный виджет, в середине - «элементы», а справа - «блоки рендеринга»):

введите описание изображения здесь

Дело в том, что: когда вы создаете новый виджет; flutter сравнит его со старым. Повторно используйте его «Element», который указывает на RenderBox. И мутировать свойства RenderBox.


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

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

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

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

Оберните себя InheritedWidgetвнутри, StatefulWidgetкоторое будет создавать новое, InheritedWidgetкогда что-то изменится!

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

class MyInherited extends StatefulWidget {
  static MyInheritedData of(BuildContext context) =>
      context.inheritFromWidgetOfExactType(MyInheritedData) as MyInheritedData;

  const MyInherited({Key key, this.child}) : super(key: key);

  final Widget child;

  @override
  _MyInheritedState createState() => _MyInheritedState();
}

class _MyInheritedState extends State<MyInherited> {
  String myField;

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

  @override
  Widget build(BuildContext context) {
    return MyInheritedData(
      myField: myField,
      onMyFieldChange: onMyFieldChange,
      child: widget.child,
    );
  }
}

class MyInheritedData extends InheritedWidget {
  final String myField;
  final ValueChanged<String> onMyFieldChange;

  MyInheritedData({
    Key key,
    this.myField,
    this.onMyFieldChange,
    Widget child,
  }) : super(key: key, child: child);

  static MyInheritedData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedData>();
  }

  @override
  bool updateShouldNotify(MyInheritedData oldWidget) {
    return oldWidget.myField != myField ||
        oldWidget.onMyFieldChange != onMyFieldChange;
  }
}

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

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

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

Реми Русселе
источник
1
Но разве создание нового InheritedWidget не приведет к перестройке всего дерева? Зачем тогда нужны Слушатели?
Thomas
1
Для вашего первого комментария я добавил к своему ответу третью часть. Что до утомительности: я не согласен. Фрагмент кода может сгенерировать это довольно легко. А получить доступ к данным так же просто, как позвонить MyInherited.of(context).
Реми Русселе
3
Не уверен, что вам интересно, но обновил образец с помощью этой техники: github.com/brianegan/flutter_architecture_samples/tree/master/… Теперь точно меньше дублирования! Если у вас есть какие-либо другие предложения для этой реализации, я бы хотел просмотреть код, если у вас когда-нибудь будет несколько свободных минут :) Все еще пытаюсь выяснить лучший способ поделиться этой логической кросс-платформой (Flutter и Web) и убедиться, что ее можно протестировать ( особенно асинхронный материал).
brianegan
4
Поскольку updateShouldNotifyтест всегда относится к одному и тому же MyInheritedStateэкземпляру, не всегда falseли он возвращается ? Конечно, buildметод MyInheritedStateсоздания новых _MyInheritedэкземпляров, но dataполе всегда ссылается на thisнет? У меня проблемы ... Работает, если я просто кодирую true.
cdock
2
@cdock Да, плохо. Не помню, зачем я это сделал, это явно не сработает. Исправлено редактированием на true, спасибо.
Реми Русселе
20

TL; DR

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


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

  1. Объекты визуализации - это то, что на самом деле отображается на экране. Они изменяемы , содержат логику рисования и макета. Дерево рендеринга очень похоже на объектную модель документа (DOM) в Интернете, и вы можете рассматривать объект рендеринга как узел DOM в этом дереве.
  2. Виджет - это описание того, что нужно визуализировать. Они неизменны и дешевы. Итак, если виджет отвечает на вопрос «Что?» (Декларативный подход), то объект Render отвечает на вопрос «Как?» (Императивный подход). Аналогия из Интернета - это «виртуальный DOM».
  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 . InheritedElement вызывает updateShouldNotify для виджета каждый раз, когда мы создаем новый виджет (вызываем setState для предка). Когда updateShouldNotify возвращает true, InheritedElement выполняет итерацию по зависимостям (?) И вызывает для него метод didChangeDependencies .

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

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

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

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

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

Код для 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, aspect))
  3. Мы возвращаем виджет предка как результат inheritFromWidgetOfExactType (return ancestor.widget)

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

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

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

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

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

  1. Кешировать виджеты руками (вручную)
  2. Используйте const, потому что const создает единственный экземпляр значения / класса
Максимр
источник
1
отличное объяснение максимр. Больше всего меня смущает то, что если все поддерево все равно перестроено при замене наследуемого виджета, в чем смысл updateShouldNotify ()?
Panda World
3

Из документов :

[BuildContext.inheritFromWidgetOfExactType] получает ближайший виджет данного типа, который должен быть типом конкретного подкласса InheritedWidget, и регистрирует этот контекст сборки с этим виджетом таким образом, что при изменении этого виджета (или введении нового виджета этого типа) или виджет уходит), этот контекст сборки перестраивается, чтобы он мог получать новые значения от этого виджета.

Обычно это неявно вызывается из статических методов of (), например Theme.of.

Как отмечалось в OP, InheritedWidgetэкземпляр не меняется ... но его можно заменить новым экземпляром в том же месте в дереве виджетов. Когда это произойдет, возможно, потребуется перестроить зарегистрированные виджеты. InheritedWidget.updateShouldNotifyМетод делает это определение. (См .: документы )

Итак, как можно заменить экземпляр? InheritedWidgetЭкземпляр может быть , удерживаемый StatefulWidget, который может заменить старый экземпляр с новым экземпляром.

Куриан
источник
-1

InheritedWidget управляет централизованными данными приложения и передает их потомку, как будто мы можем хранить здесь количество корзин, как описано здесь :

Сунил
источник