Флаттер: как правильно использовать Inherited Widget?
Как правильно использовать InheritedWidget? До сих пор я понимал, что это дает вам возможность распространять данные по дереву виджетов. В крайнем случае, если вы укажете RootWidget, он будет доступен из всех виджетов в дереве на всех маршрутах, и это нормально, потому что каким-то образом я должен сделать свою ViewModel/Model доступной для своих виджетов, не прибегая к глобальным или синглетонам.
НО InheritedWidget является неизменным, так как я могу его обновить? И что более важно, как мои Stateful Widgets запускаются для восстановления их поддеревьев?
К сожалению, документация здесь очень непонятна, и после большого обсуждения никто не знает, как правильно ее использовать.
Я добавляю цитату из Брайана Игана:
Да, я вижу это как способ распространения данных по дереву. Что меня смущает из документации по API:
"Унаследованные виджеты, когда на них ссылаются таким образом, заставят потребителя перестраиваться, когда сам унаследованный виджет изменяет состояние".
Когда я впервые прочитал это, я подумал:
Я мог бы вставить некоторые данные в InheritedWidget и изменить его позже. Когда эта мутация произойдет, она перестроит все виджеты, которые ссылаются на мой InheritedWidget. Что я нашел:
Чтобы изменить состояние InheritedWidget, вам нужно обернуть его в StatefulWidget. Затем вы фактически изменяете состояние StatefulWidget и передаете эти данные в InheritedWidget, который передает данные всем его дочерним элементам. Однако в этом случае, кажется, перестраивается все дерево под StatefulWidget, а не только виджеты, которые ссылаются на InheritedWidget. Это верно? Или он каким-то образом узнает, как пропустить виджеты, которые ссылаются на InheritedWidget, если updateShouldNotify возвращает false?
1 ответ
Проблема исходит из вашей цитаты, которая неверна.
Как вы сказали, InheritedWidgets, как и другие виджеты, являются неизменяемыми. Поэтому они не обновляются. Они созданы заново.
Дело в том, что InheritedWidget - это простой виджет, который ничего не делает, кроме хранения данных. У него нет никакой логики обновления или чего-то еще. Но, как и любые другие виджеты, это связано с Element
, И угадай что? Эта вещь изменчива, и флаттер будет использовать ее всякий раз, когда это возможно!
Исправленная цитата будет:
InheritedWidget, если на него ссылаются таким образом, заставит потребителя перестраивать при изменении InheritedWidget, связанного с InheritedElement.
Там много говорят о том, как виджеты / элементы /renderbox объединены вместе. Но вкратце, они похожи на это (слева - типичный виджет, в середине - "элементы", а справа - "рендер-боксы"):
Дело в том, что: когда вы создаете новый виджет; флаттер сравнит его со старым. Повторно используйте это "Элемент", который указывает на 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 потенциально может быть тот же самый дочерний элемент, что и раньше. И под точным я имею в виду тот же пример. Виджеты, которые имеют тот же экземпляр, что и раньше, не перестраиваются.
И в большинстве случаев (имея унаследованный виджет в корне вашего приложения) унаследованный виджет является постоянным. Так что нет необходимости перестраивать.
TL;DR
Не используйте тяжелые вычисления внутри метода updateShouldNotify и используйте cont вместо new при создании виджета
Прежде всего, мы должны понять, что такое объекты Widget, Element и Render.
- Объекты рендеринга - это то, что фактически отображается на экране. Они изменчивы, содержат логику рисования и компоновки. Дерево рендеринга очень похоже на объектную модель документа (DOM) в Интернете, и вы можете рассматривать объект рендеринга как узел DOM в этом дереве.
- Виджет - это описание того, что должно быть отображено. Они неизменны и дешевы. Таким образом, если виджет отвечает на вопрос "Что?"(Декларативный подход), тогда объект Render отвечает на вопрос "Как?"(Императивный подход). Аналогия из Интернета - "Виртуальный ДОМ".
- Element / BuildContext - это прокси между объектами Widget и Render. Он содержит информацию о положении виджета в дереве * и о том, как обновить объект Render при изменении соответствующего виджета.
Теперь мы готовы погрузиться в метод InheritedWidget и метод BuildContext в формате garitFromWidgetOfExactType.
В качестве примера я рекомендую рассмотреть этот пример из документации 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;
}
- Мы добавляем предка как зависимость текущего элемента (_dependencies.add(ancestor))
- Мы добавляем текущий элемент в зависимости от предка (ancestor.updateDependencies(this, аспект))
- Мы возвращаем виджет предшественника как результат функцииирателя_ответа_Объявления (возвращаем ancestor.widget)
Итак, теперь мы знаем, где InheritedElement получает свои зависимости.
Теперь давайте посмотрим на метод didChangeDependencies. Каждый элемент имеет этот метод:
void didChangeDependencies() {
assert(_active); // otherwise markNeedsBuild is a no-op
assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
markNeedsBuild();
}
Как мы видим, этот метод просто помечает элемент как грязный, и этот элемент должен быть перестроен в следующем кадре. Перестройка означает создание метода вызова на элементе виджета coresponding.
Но как насчет "Перестроения всего поддерева, когда я перестраиваю InheritedWidget?". Здесь следует помнить, что виджеты являются неизменяемыми, и если вы создадите новый виджет, Flutter перестроит поддерево. Как мы можем это исправить?
- Кешировать виджеты руками (вручную)
- Используйте const, потому что const создает единственный экземпляр значения / класса
Из документов:
[BuildContext.inheritFromWidgetOfExactType] получает ближайший виджет заданного типа, который должен быть типом конкретного подкласса InheritedWidget, и регистрирует этот контекст компоновки с этим виджетом таким образом, чтобы при изменении этого виджета (или вводе нового виджета этого типа, или виджет исчезает), этот контекст сборки перестраивается, чтобы он мог получать новые значения из этого виджета.
Обычно это вызывается неявно из статических методов of(), например, Theme.of.
Как отметил ФП, InheritedWidget
Экземпляр не изменяется... но его можно заменить новым экземпляром в том же месте в дереве виджетов. Когда это происходит, возможно, что зарегистрированные виджеты необходимо перестроить. InheritedWidget.updateShouldNotify
метод делает это определение. (См.: документы)
Так как же заменить экземпляр? InheritedWidget
Экземпляр может содержаться StatefulWidget
, который может заменить старый экземпляр новым.
InheritedWidget управляет централизованными данными приложения и передает их потомку, как будто мы можем хранить здесь количество корзин, как описано здесь: