InheritedWidget — это специальный виджет во Flutter, который передаёт данные вниз по дереву виджетов без явной передачи через конструкторы. Дочерние виджеты получают доступ к данным через BuildContext и автоматически подписываются на обновления. Когда данные в InheritedWidget изменяются, все зависимые виджеты перестраиваются. По данным Flutter API Reference, 2025, InheritedWidget лежит в основе Theme, MediaQuery, Localizations и большинства библиотек управления состоянием.
Главное
InheritedWidget — это виджет, который делает свои данные доступными для всех потомков в Widget Tree. В отличие от обычного виджета, который передаёт данные только через конструктор дочерним элементам, InheritedWidget позволяет любому виджету в поддереве получить доступ к данным без цепочки параметров. Это решает проблему "prop drilling" — передачи данных через множество промежуточных виджетов, которые сами не используют эти данные.
Flutter включает несколько встроенных InheritedWidget: Theme (цветовая схема и стили), MediaQuery (размер экрана, ориентация, плотность пикселей), Localizations (локализованные строки), Directionality (направление текста), DefaultTextStyle (стиль текста по умолчанию). Эти виджеты устанавливаются корневыми виджетами вроде MaterialApp и доступны во всём приложении.
InheritedWidget не имеет собственного состояния — он хранит данные, переданные через конструктор. Когда родитель InheritedWidget перестраивается с новыми данными, вызывается метод updateShouldNotify для сравнения старых и новых данных. Если метод возвращает true, все зависимые виджеты помечаются для перестроения. Это простой, но эффективный механизм реактивного обновления.
Механизм передачи данных через InheritedWidget основан на Element Tree. Когда виджет вызывает dependOnInheritedWidgetOfExactType, соответствующий элемент регистрирует зависимость от InheritedElement. При изменении InheritedWidget, InheritedElement уведомляет все зависимые элементы, которые перестраиваются в следующем кадре.
Метод dependOnInheritedWidgetOfExactType не просто находит InheritedWidget в дереве — он подписывает текущий элемент на уведомления. Если бы вы использовали findAncestorWidgetOfExactType вместо dependOn, виджет получил бы данные, но не перестраивался бы при их изменении. Это важное различие: dependOn — это подписка, а findAncestor — одноразовый поиск.
Когда виджет запрашивает InheritedWidget, Flutter поднимается по Element Tree от текущего элемента до корня, проверяя каждый InheritedElement на совпадение типа. Первый найденный InheritedElement возвращается. Это означает, что ближайший InheritedWidget в дереве имеет приоритет — вы можете переопределить данные на определённом уровне, разместив InheritedWidget ближе к потомкам.
class ThemeData {
final Color primaryColor;
final TextTheme textTheme;
const ThemeData({required this.primaryColor, required this.textTheme});
}
class MyTheme extends InheritedWidget {
final ThemeData data;
const MyTheme({required this.data, required Widget child}) : super(child: child);
static MyTheme of(BuildContext context) {
final widget = context.dependOnInheritedWidgetOfExactType<MyTheme>();
assert(widget != null, "MyTheme not found in tree");
return widget!;
}
@override
bool updateShouldNotify(MyTheme oldWidget) => oldWidget.data != data;
}
В этом примере MyTheme использует статический метод of для предоставления данных потомкам. Метод dependOnInheritedWidgetOfExactType регистрирует зависимость, а updateShouldNotify сравнивает старые и новые данные для определения необходимости перестроения зависимых виджетов.
Создание собственного InheritedWidget состоит из двух шагов: определение класса, наследующего InheritedWidget, и реализация статического метода of для доступа из потомков. Данные передаются через конструктор, а метод updateShouldNotify определяет, когда зависимые виджеты должны перестраиваться.
Класс должен наследовать InheritedWidget и принимать данные через конструктор с обязательным параметром child. Данные могут быть любого типа: примитивы, объекты, функции. Главное правило — данные должны быть неизменяемыми (immutable), чтобы можно было надёжно сравнивать старое и новое значение.
Статический метод of принимает BuildContext и возвращает данные InheritedWidget. Внутри вызывается dependOnInheritedWidgetOfExactType, который ищет ближайший InheritedWidget указанного типа в дереве. Если InheritedWidget не найден, метод выбрасывает исключение или возвращает значение по умолчанию в зависимости от реализации.
Для доступа к данным виджет вызывает MyWidget.of(context) внутри метода build. Flutter автоматически подписывает виджет на обновления. Если данные изменятся, виджет перестроится в следующем кадре. Это позволяет создавать чистый и декларативный код без лишних параметров.
class UserPreferences extends InheritedWidget {
final String languageCode;
final bool darkMode;
const UserPreferences({
required this.languageCode,
required this.darkMode,
required Widget child,
}) : super(child: child);
static UserPreferences of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserPreferences>()!;
}
@override
bool updateShouldNotify(UserPreferences oldWidget) =>
oldWidget.languageCode != languageCode || oldWidget.darkMode != darkMode;
}
В этом примере UserPreferences хранит настройки пользователя. Метод updateShouldNotify сравнивает каждое поле по отдельности, что предотвращает лишние перестроения при изменении только одного параметра. Используйте аналогичный подход для собственных InheritedWidget с несколькими полями.
updateShouldNotify — это ключевой метод InheritedWidget, который определяет, нужно ли уведомлять зависимые виджеты об изменении данных. Если метод возвращает false, зависимые виджеты не перестраиваются, даже если сам InheritedWidget получил новый экземпляр с теми же данными. Это критически важно для производительности.
Сравнивайте только те поля, которые действительно изменились и влияют на отображение. Если InheritedWidget содержит 10 полей, но только одно из них влияет на UI, проверяйте только это поле. Для коллекций используйте глубокое сравнение или иммутабельные структуры данных. Не используйте == для List или Map, так как они сравниваются по ссылке.
Самая частая ошибка — возвращать true без сравнения. Это приводит к перестроению всех зависимых виджетов при каждом обновлении родителя, даже если данные не изменились. Вторая ошибка — возвращать false, когда данные изменились, что приводит к устаревшему UI. Третья — сложное сравнение, которое выполняется каждый кадр и замедляет работу.
InheritedWidget и колбэки (передача функций через конструктор) решают разные задачи. InheritedWidget подходит для данных, которые нужны многим виджетам на разных уровнях дерева. Колбэки удобны для однонаправленной передачи событий от родителя к конкретному потомку или наоборот. Выбор зависит от архитектуры приложения и частоты изменений.
Используйте InheritedWidget, когда данные нужны многим виджетам на разных уровнях вложенности: тема приложения, настройки пользователя, информация об устройстве, данные текущей сессии. InheritedWidget особенно эффективен для "глобальных" данных, которые изменяются редко, но нужны в разных частях UI.
Колбэки (функции обратного вызова) подходят для передачи событий от дочернего виджета к родительскому: нажатие кнопки, выбор элемента списка, отправка формы. Колбэки явно указывают, какие действия может выполнить потомок, и не создают скрытых зависимостей. Для передачи данных вниз по дереву на небольшое количество уровней также проще использовать параметры конструктора.
| Критерий | InheritedWidget | Колбэки |
|---|---|---|
| Направление | Сверху вниз (родитель → потомки) | Снизу вверх (потомок → родитель) или непосредственно |
| Область | Всё поддерево | Конкретный виджет |
| Перестроение | Автоматическое при изменении данных | Требуется setState вручную |
| Сложность | Средняя (нужен класс InheritedWidget) | Низкая (просто функция) |
Provider и Riverpod — популярные библиотеки управления состоянием во Flutter, построенные поверх InheritedWidget. Они расширяют его возможности: добавляют поддержку ChangeNotifier, автоматическое удаление при демонтировании, ленивую инициализацию и упрощённый синтаксис через генерики.
Provider использует InheritedWidget для передачи объекта любого типа вниз по дереву. ChangeNotifierProvider отслеживает изменения через ChangeNotifier и вызывает updateShouldNotify при вызове notifyListeners. Это избавляет разработчика от ручного создания InheritedWidget и реализации updateShouldNotify.
Прямой InheritedWidget даёт больше контроля и не требует внешних зависимостей. Provider предоставляет готовую инфраструктуру: Consumer, Selector, MultiProvider, ProxyProvider. Выбор зависит от сложности приложения. Для простых проектов прямой InheritedWidget достаточен, для крупных — Provider или Riverpod снижают шаблонный код.
// Direct InheritedWidget
class UserProvider extends InheritedWidget {
final UserData userData;
const UserProvider({required this.userData, required Widget child}) : super(child: child);
static UserData of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<UserProvider>()!.userData;
@override
bool updateShouldNotify(UserProvider old) => old.userData != userData;
}
// Provider equivalent
return ChangeNotifierProvider<UserData>(
create: (_) => UserData(),
child: MyApp(),
);
Оба подхода в примере решают одну задачу — передачу UserData вниз по дереву. Provider сокращает количество кода, но скрывает механику InheritedWidget. Прямой InheritedWidget даёт полный контроль и понимание происходящего, что особенно важно при обучении Flutter и отладке сложных проблем с перестроением.
Часто задаваемые вопросы
InheritedWidget делает данные доступными для всех потомков через BuildContext, а обычный виджет передаёт данные только через конструктор. InheritedWidget также подписывает потомков на обновления данных.
Зависимые виджеты перестраиваются только когда updateShouldNotify возвращает true. Если метод реализован правильно, перестроение происходит только при реальном изменении данных, а не при каждом rebuild родителя.
Да, можно использовать любое количество InheritedWidget в одном дереве. Каждый предоставляет данные определённого типа, а виджеты могут получать данные из нескольких InheritedWidget одновременно.
dependOn подписывает виджет на обновления — при изменении данных виджет перестроится. findAncestor выполняет одноразовый поиск без подписки, и виджет не узнает об изменениях данных.
Для простого состояния (тема, настройки) InheritedWidget достаточен. Для сложного состояния с бизнес-логикой используйте Provider, Riverpod или BLoC — они построены на InheritedWidget и добавляют необходимую инфраструктуру.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также