Флаттер: Як правильно використовувати успадкований віджет?


103

Який правильний спосіб використовувати InheritedWidget? Поки я зрозумів, що це дає вам можливість розповсюджувати дані по дереву віджетів. В крайньому випадку, якщо ви ставите як RootWidget, він буде доступний з усіх віджетів у дереві на всіх маршрутах, що чудово, оскільки якимось чином я повинен зробити свій ViewModel / Model доступним для моїх віджетів, не вдаючись до глобальних чи синглтон.

АЛЕ InheritedWidget є незмінним, то як я можу його оновити? І що важливіше, як мої державні віджети запускаються для відновлення своїх піддерев?

На жаль, документація тут дуже незрозуміла, і після обговорення з багатьма, здається, насправді ніхто не знає, який правильний спосіб її використання.

Я додаю цитату Брайана Ігана:

Так, я розглядаю це як спосіб поширення даних по дереву. Що мене бентежить з документів API:

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

Коли я вперше прочитав це, я подумав:

Я міг би вкласти деякі дані в InheritedWidget і змінити їх пізніше. Коли відбудеться така мутація, вона відновить усі віджети, які посилаються на мій InheritedWidget Те, що я знайшов:

Для того, щоб змінити стан InheritedWidget, вам потрібно обернути його у StatefulWidget. Потім ви насправді мутуєте стан StatefulWidget і передаєте ці дані до InheritedWidget, який передає дані всім своїм дітям. Однак у такому випадку, схоже, відбудовується все дерево під StatefulWidget, а не лише віджети, що посилаються на InheritedWidget. Це правильно? Або він якось знатиме, як пропустити віджети, що посилаються на InheritedWidget, якщо updateShouldNotify повертає false?

Відповіді:


108

Проблема пов’язана з вашою пропозицією, яка є неправильною.

Як ви вже сказали, InheritedWidgets, як і інші віджети, незмінні. Тому вони не оновлюються . Вони створюються заново.

Річ у тому, що InheritedWidget - це просто простий віджет, який не робить нічого, крім зберігання даних . Він не має жодної логіки оновлення чи чогось іншого. Але, як і будь-які інші віджети, він пов'язаний із Element. І вгадайте, що? Цю річ можна змінювати, і тріпотіння буде використовувати її по можливості, коли це можливо!

Виправлена ​​цитата буде такою:

InheritedWidget, якщо посилання здійснюється таким чином, призведе до відновлення споживача при зміні InheritedWidget, пов’язаного з InheritedElement .

Існує чудова розмова про те, як віджети / елементи / візуалізатор підключаються . Але коротше, вони такі (зліва - це типовий віджет, посередині - "елементи", а праворуч - "візуалізатори"):

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

Справа в тому, що коли ви створюєте інстанцію нового віджета; флаттер порівняє його зі старим. Повторно використовуйте його "Елемент", який вказує на RenderBox. І змінити властивості RenderBox.


Гаразд, але як це відповідає на моє запитання?

При створенні екземпляра InheritedWidget, а потім виклику context.inheritedWidgetOfExactType(або MyClass.ofякий в основному однаковий); мається на увазі, що він буде слухати Elementасоційований з вами InheritedWidget. І щоразу, коли це Elementотримує новий віджет, він примушує оновити будь-які віджети, які викликали попередній метод.

Коротше кажучи, коли ви замінюєте існуюче InheritedWidgetна нове; трепет побачить, що він змінився. І повідомить пов’язані віджети про можливу модифікацію.

Якщо ви все зрозуміли, ви вже мали здогадатися про рішення:

Загорніть 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 відновлення цілого дерева? Навіщо тоді необхідність слухачів?
Томас

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, спасибі.
Rémi Rousselet

20

TL; DR

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


Перш за все, ми повинні зрозуміти, що таке віджети, елементи та візуалізація.

  1. Об'єкти візуалізації - це те, що насправді відображається на екрані. Вони змінюються , містять логіку малювання та компонування. Дерево візуалізації дуже схоже на об'єктну модель документа (DOM) в Інтернеті, і ви можете розглядати об'єкт візуалізації як вузол DOM у цьому дереві
  2. Віджет - це опис того, що слід відобразити. Вони незмінні та дешеві. Отже, якщо віджет відповідає на питання "Що?" (Декларативний підхід), то об'єкт "Візуалізація" відповідає на питання "Як?" (Імперативний підхід). Аналогією з Інтернету є "Віртуальний DOM".
  3. Element / BuildContext - це проксі-сервер між об'єктами Widget та Render . Він містить інформацію про позицію віджета у дереві * та про те, як оновити об’єкт Render при зміні відповідного віджета.

Тепер ми готові зануритися в InheritedWidget і метод BuildContext nasleditiFromWidgetOfExactType .

Як приклад я рекомендую розглянути цей приклад із документації 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 отримує залежності ? Тут ми повинні розглянути метод nasleditiFromWidgetOfExactType .

nasleditiFromWidgetOfExactType - Цей метод визначено в 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, що відображається за типом. Якщо предка знайдено, ми тоді викликаємо nasleditFromElement .

Код для 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 (предка))
  2. Ми додаємо поточний елемент до залежностей предків (ancestor.updateDependencies (цей, аспект))
  3. Ми повертаємо віджет предка як результат наслідуванняFromWidgetOfExactType (повернення предка.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, і реєструє цей контекст побудови за допомогою цього віджета, таким чином, що коли цей віджет змінюється (або вводиться новий віджет цього типу, або віджет зникає), цей контекст побудови перебудовується, щоб він міг отримувати нові значення з цього віджета.

Це зазвичай називається неявно із () статичних методів, наприклад Theme.of.

Як зазначає OP, InheritedWidgetекземпляр не змінюється ... але його можна замінити новим екземпляром у тому ж місці в дереві віджетів. Коли це трапляється, можливо, що зареєстровані віджети потрібно перебудувати. InheritedWidget.updateShouldNotifyМетод робить це визначення. (Див .: документи )

То як можна замінити екземпляр? InheritedWidgetПримірник може бути , утримуваний StatefulWidget, який може замінити старий екземпляр з новим екземпляром.


-1

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.