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

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

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

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

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

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

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

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

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

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


person Thomas    schedule 26.03.2018    source источник
comment
Отличный вопрос! Спасибо за вопрос.   -  person Pratik Butani    schedule 17.12.2020


Ответы (4)


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

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

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

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

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

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

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

Дело в том, что когда вы создаете новый виджет; 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 потенциально может иметь того же дочернего элемента, что и раньше. И под точным я подразумеваю тот же самый случай. Виджеты, у которых есть тот же экземпляр, что и раньше, не восстанавливаются.

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

person Rémi Rousselet    schedule 26.03.2018
comment
Но разве создание нового InheritedWidget не приведет к перестройке всего дерева? Зачем тогда нужны Слушатели? - person Thomas; 26.03.2018
comment
Хорошо, я думаю, вы можете изменить состояние виджетов Statefull вниз по дереву. Но по-прежнему кажется довольно утомительным распространять изменения состояния таким образом. - person Thomas; 26.03.2018
comment
Для вашего первого комментария я добавил к своему ответу третью часть. Что до утомительности: я не согласен. Фрагмент кода может сгенерировать это довольно легко. А получить доступ к данным так же просто, как позвонить MyInherited.of(context). - person Rémi Rousselet; 26.03.2018
comment
Чтобы дать ему тот же экземпляр, что и дочерний, мне пришлось бы спасти ребенка перед воссозданием InheritedWidget - person Thomas; 26.03.2018
comment
›И в большинстве случаев (при наличии унаследованного виджета в корне вашего приложения) унаследованный виджет является постоянным. Так что никакой ненужной перестройки ‹Но как тогда изменить его значения? я - person Thomas; 26.03.2018
comment
@ RémiRousselet Спасибо за отличное объяснение - объясняет, почему я каждый раз видел перестройки (я воссоздавал child при каждой перестройке). Попробую еще раз с постоянным ребенком. Есть ли у вас опубликованные генераторы кода? Было бы интересно посмотреть :) - person brianegan; 26.03.2018
comment
В обычной настройке InheritedWidget > MaterialApp > View дочерний элемент унаследованного виджета является постоянным. И это примерно в 99% случаев. - person Rémi Rousselet; 26.03.2018
comment
Итак, вы сохраняете экземпляр MaterialApp и используете его при изменении унаследованного виджета? - person Thomas; 26.03.2018
comment
Генератор кода @brianegan? Зачем? Предоставленный мной код можно сгенерировать с помощью простого статического фрагмента кода vscode. И в нем нет дублирования кода. Не вижу смысла иметь здесь генератор кода. - person Rémi Rousselet; 26.03.2018
comment
@ RémiRousselet Попался. Немного неправильно прочитал часть о состоянии, подумал, что есть небольшое дублирование. Это интересный метод захвата класса State ... Я дублировал кое-что из этого в моем InheritedWidget, чтобы пики не могли получить доступ к таким методам, как initState. Я попробую применить его к образцам архитектуры Flutter и посмотрю, как это получится. Еще раз спасибо за подробное объяснение :) - person brianegan; 26.03.2018
comment
Я здесь ничего не придумал. Изучив код Flutter (особенно Theme / Navigator), я понял, как они используют InheritedWidget. Не только избегает дублирования. Но это также позволяет extend наследуемый виджет. Например, Theme расширить IconTheme, не нарушая IconTheme.of(context)! - person Rémi Rousselet; 26.03.2018
comment
Не уверен, что вам интересно, но обновил образец с помощью этой техники: github .com / brianegan / flutter_architecture_samples / tree / master / Теперь точно меньше дублирования! Если у вас есть какие-либо другие предложения для этой реализации, я хотел бы получить обзор кода, если у вас когда-нибудь будет несколько свободных минут :) Все еще пытаюсь выяснить лучший способ поделиться этой логической кросс-платформой (Flutter и Web) и убедиться, что ее можно протестировать ( особенно асинхронный материал). - person brianegan; 26.03.2018
comment
Посмотрев на образец @brianegan, я думаю, что теперь это понимаю. ссылаясь на widget.child, вы получаете каждый раз один и тот же дочерний элемент, который гарантирует, что он не будет перестроен. - person Thomas; 26.03.2018
comment
Я не уверен, стоит ли использовать тип StatefulWidgets type for the .of() method instead of the InheritedWidgets, чтобы прояснить, что речь идет о передаваемых данных. - person Thomas; 26.03.2018
comment
Поскольку updateShouldNotify тест всегда ссылается на один и тот же MyInheritedState экземпляр, не всегда ли он будет возвращать false? Конечно, build метод MyInheritedState создает новые _MyInherited экземпляры, но поле data всегда ссылается на this нет? У меня проблемы ... Работает, если я просто запрограммирую true. - person cdock; 27.03.2018
comment
@cdock Да, плохо. Не помню, зачем я это сделал, потому что это явно не сработает. Исправлено редактированием на true, спасибо. - person Rémi Rousselet; 27.03.2018
comment
В моем of(context) вызове я получаю эту ошибку: NoSuchMethodError: The getter 'data' was called on null. - person CodeGrue; 14.08.2018
comment
Я добавил String get stuff => 'test'; к _MyInherited и изменил метод of, чтобы он возвращал stuff вместо data, и я все еще получаю NoSuchMethodError: The getter 'stuff' was called on null., что не имеет большого смысла, поскольку все жестко запрограммировано, поэтому не может быть нулевым. Обратите внимание, что я также проверил, что _MyInherited не возвращается как null внутри метода of. - person CodeGrue; 14.08.2018
comment
Итак, похоже, что с MaterialApp как дочерним элементом для MyInherited, доступ к MyInherited.of() для свойства MaterialApp вызывает эту проблему. @brianegan Я заметил в вашем коде дополнительный слой виджетов под названием InheritedWidgetApp, поэтому я добавил его в свой код и исправил эту проблему. Почему? - person CodeGrue; 14.08.2018
comment
Было бы хорошим решением использовать эту технику, чтобы иметь настраиваемый виджет, такой как настраиваемый контейнер, где по умолчанию есть граница. Поэтому вместо того, чтобы применять одну и ту же границу к каждому контейнеру, просто используйте вместо этого этот настраиваемый контейнер. - person stuckedoverflow; 11.12.2018
comment
Так что / где onMyFieldChange когда-либо назывался? - person SimonH; 17.07.2019
comment
Лучшая причина никогда не беспокоиться о InheritedWidget - это Provider: stackoverflow.com/a/57159539/2301224 - person Baker; 10.03.2020
comment
Если у меня есть 20 полей для хранения, тогда у меня должно быть 20 полей и 20 методов onMyFieldChange как в MyInheritedData, так и в _MyInheritedState, верно? - person Alexander Pravdin; 13.07.2020
comment
Помимо 20 полей в двух классах и 20 методов onChangeXX, я должен добавить 20 аргументов конструктора MyInheritedData. Это означает 120+ строк на 20 настроек. Слишком много. Есть ли лучшее решение? - person Alexander Pravdin; 14.07.2020

TL; DR

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


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

  1. Объекты Render - это то, что фактически отображается на экране. Они изменяемые, содержат логику рисования и макета. Дерево рендеринга очень похоже на объектную модель документа (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();
  }

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

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

  1. Кешировать виджеты руками (вручную)
  2. Используйте const, потому что const создает единственный экземпляр значения / класса.
person maksimr    schedule 11.12.2018
comment
отличное объяснение максимр. Больше всего меня смущает то, что если все поддерево все равно было перестроено при замене inheritedWidget, в чем смысл updateShouldNotify ()? - person Panda World; 03.12.2019
comment
Итак, здесь унаследованный виджет может обновлять своего слушателя при изменении значения, и это именно виджет провайдера, так какая разница между ними. .поправьте меня если я ошибаюсь - person Omar Essam; 08.07.2021

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

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

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

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

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

person kkurian    schedule 03.03.2019

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

person Sunil    schedule 05.01.2020