Перевод статьи подготовлен для студентов курса «Flutter Mobile Developer».
Корни деревьев виджетов во Flutter могут уходить очень глубоко…
Очень глубоко.
Компонентная природа виджетов Flutter позволяет создавать очень элегантный, модульный и гибкий дизайн приложений. Однако это также может вылиться в появление большого количества шаблонного кода для передачи контекста. Посмотрите, что происходит, когда мы хотим передать accountId и scopeId со страницы в виджет двумя уровнями ниже:
Если не держать его под контролем, этот шаблон может очень легко расползтись по всей кодовой базе. Лично мы параметризовали более 30 виджетов таким образом. Почти половину рабочего времени виджет получал параметры только для того, чтобы передать их далее, как в
Конечно, должен быть способ получше…
Представляю вам InheritedWidget.
В двух словах, это особый вид виджета, который определяет контекст в корне поддерева. Он может эффективно предоставлять этот контекст каждому виджету в этом поддереве. Шаблон доступа должен выглядеть знакомым для Flutter разработчика:
Этот контекст — просто класс Dart. Таким образом, он может содержать все, что вы захотите туда запихнуть. Многие из часто используемых контекстов Flutter, такие как
Если дополнить вышеприведенный пример, используя InheritedWidget, вот что мы получим:
Важно отметить:
Наконец, давайте поговорим о хороших практиках.
Перегрузка их большим количеством контекста приводит к потере второго и третьего преимуществ, упомянутых выше, поскольку Flutter не может определить, какая часть контекста обновляется, а какая часть используется виджетами. Вместо:
Предпочитайте делать:
Без const выборочное перестроение поддерева не происходит. Flutter создает новый экземпляр каждого виджета в поддереве и вызывает
Следите за областью видимости ваших
Вы должны выбирать одну или другую в зависимости от того, где применим контекст.
Это кажется очевидным. Однако это имеет серьезные последствия, потому что большинство приложений имеют более одного уровня навигации. Вот как могло бы выглядеть ваше приложение:
Вот что видит Flutter:
С точки зрения Flutter иерархии навигации не существует. Каждая страница (или scaffold) представляет собой дерево виджетов, привязанное к виджету приложения. Следовательно, когда вы используете
Хотя существуют разные способы передачи контекста, я предлагаю параметризовать маршруты старомодным способом (например, URL кодированием, если вы используете именованные маршруты). Это также гарантирует, что страницы могут быть построены исключительно на основе маршрута без необходимости использовать контекст их родительской страницы.
Удачного вам кодинга!
Успеть на курс!
Корни деревьев виджетов во Flutter могут уходить очень глубоко…
Очень глубоко.
Компонентная природа виджетов Flutter позволяет создавать очень элегантный, модульный и гибкий дизайн приложений. Однако это также может вылиться в появление большого количества шаблонного кода для передачи контекста. Посмотрите, что происходит, когда мы хотим передать accountId и scopeId со страницы в виджет двумя уровнями ниже:
class MyPage extends StatelessWidget {
final int accountId;
final int scopeId;
MyPage(this.accountId, this.scopeId);
Widget build(BuildContext context) {
return new MyWidget(accountId, scopeId);
}
}
class MyWidget extends StatelessWidget {
final int accountId;
final int scopeId;
MyWidget(this.accountId, this.scopeId);
Widget build(BuildContext context) {
// где-нибудь недалеко в коде
new MyOtherWidget(accountId, scopeId);
...
}
}
class MyOtherWidget extends StatelessWidget {
final int accountId;
final int scopeId;
MyOtherWidget(this.accountId, this.scopeId);
Widget build(BuildContext context) {
// и повторить
...
Если не держать его под контролем, этот шаблон может очень легко расползтись по всей кодовой базе. Лично мы параметризовали более 30 виджетов таким образом. Почти половину рабочего времени виджет получал параметры только для того, чтобы передать их далее, как в
MyWidget
из примера, приведенного выше.Состояние MyWidget не зависит от параметров, и тем не менее, он перестраивается каждый раз, когда меняются параметры!
Конечно, должен быть способ получше…
Представляю вам InheritedWidget.
В двух словах, это особый вид виджета, который определяет контекст в корне поддерева. Он может эффективно предоставлять этот контекст каждому виджету в этом поддереве. Шаблон доступа должен выглядеть знакомым для Flutter разработчика:
final myInheritedWidget = MyInheritedWidget.of(context);
Этот контекст — просто класс Dart. Таким образом, он может содержать все, что вы захотите туда запихнуть. Многие из часто используемых контекстов Flutter, такие как
Style
или MediaQuery
, представляют собой ни что иное, как InheritedWidget-ы, живущие на уровне MaterialApp
.Если дополнить вышеприведенный пример, используя InheritedWidget, вот что мы получим:
class MyInheritedWidget extends InheritedWidget {
final int accountId;
final int scopeId;
MyInheritedWidget(accountId, scopeId, child): super(child);
@override
bool updateShouldNotify(MyInheritedWidget old) =>
accountId != old.accountId || scopeId != old.scopeId;
}
class MyPage extends StatelessWidget {
final int accountId;
final int scopeId;
MyPage(this.accountId, this.scopeId);
Widget build(BuildContext context) {
return new MyInheritedWidget(
accountId,
scopeId,
const MyWidget(),
);
}
}
class MyWidget extends StatelessWidget {
const MyWidget();
Widget build(BuildContext context) {
// где-нибудь недалеко в коде
const MyOtherWidget();
...
}
}
class MyOtherWidget extends StatelessWidget {
const MyOtherWidget();
Widget build(BuildContext context) {
final myInheritedWidget = MyInheritedWidget.of(context);
print(myInheritedWidget.scopeId);
print(myInheritedWidget.accountId);
...
Важно отметить:
- Конструкторы теперь
const
, что делает эти виджеты кэшируемыми; что увеличивает производительность. - Когда параметры обновляются, создается новый
MyInheritedWidget
. Однако, в отличие от первого примера, поддерево не перестраивается. Вместо этого Flutter ведет внутренний реестр, который отслеживает виджеты, которые обращались к этомуInheritedWidget
-у, и перестраивает только те виджеты, которые используют этот контекст. В этом примере этоMyOtherWidget
. - Если дерево перестраивается по причине, не связанной с изменением параметров, такими как изменение ориентации, ваш код все равно может построить новый
InheritedWidget
. Однако, поскольку параметры остались прежними, виджеты в поддереве не будут уведомлены. Это цель функцииupdateShouldNotify
, реализованной вашимInheritedWidget
.
Наконец, давайте поговорим о хороших практиках.
InheritedWidget должен быть небольшой
Перегрузка их большим количеством контекста приводит к потере второго и третьего преимуществ, упомянутых выше, поскольку Flutter не может определить, какая часть контекста обновляется, а какая часть используется виджетами. Вместо:
class MyAppContext {
int teamId;
String teamName;
int studentId;
String studentName;
int classId;
...
}
Предпочитайте делать:
class TeamContext {
int teamId;
String teamName;
}
class StudentContext {
int studentId;
String studentName;
}
class ClassContext {
int classId;
...
}
Используйте const для создания ваших виджетов
Без const выборочное перестроение поддерева не происходит. Flutter создает новый экземпляр каждого виджета в поддереве и вызывает
build()
, тратя впустую драгоценные циклы, особенно если ваши методы сборки достаточно тяжелы.Следите за областью видимости ваших InheritedWidget
-ов
InheritedWidget
-ы помещаются в корень дерева виджетов. Это, по сути, и определяет их область видимости. В нашей команде мы обнаружили, что возможность объявлять контекст в любом месте дерева виджетов это уже чересчур. Мы решили ограничить наши контекстные виджеты, чтобы они принимали только Scaffold
(или его производные) в качестве дочерних элементов. Таким образом, мы гарантируем, что наиболее детализированный контекст может быть на уровне страницы, и получаем две области видимости:- Виджеты уровня приложения, такие как
MediaQuery
. Они доступны для любого виджета на любой странице вашего приложения, так как они находятся в корне дерева виджетов вашего приложения. - Виджеты уровня страницы, такие как
MyInheritedWidget
в приведенном выше примере.
Вы должны выбирать одну или другую в зависимости от того, где применим контекст.
Виджеты уровня страницы не могут преодолевать границу маршрута
Это кажется очевидным. Однако это имеет серьезные последствия, потому что большинство приложений имеют более одного уровня навигации. Вот как могло бы выглядеть ваше приложение:
> School App [App Context]
> Student [Student Context]
> Grades
> Bio
> Teacher [Teacher Context]
> Courses
> Bio
Вот что видит Flutter:
> School App [App Context]
> Student [Student Context]
> Student Grades
> Student Bio
> Teacher [Teacher Context]
> Teacher Courses
> Teacher Bio
С точки зрения Flutter иерархии навигации не существует. Каждая страница (или scaffold) представляет собой дерево виджетов, привязанное к виджету приложения. Следовательно, когда вы используете
Navigator.push
для отображения этих страниц, они не наследуют виджет, несущий родительский контекст. В приведенном выше примере, вам нужно будет в явном виде передавать контекст Student
из страницы Student в страницу Student Bio.Хотя существуют разные способы передачи контекста, я предлагаю параметризовать маршруты старомодным способом (например, URL кодированием, если вы используете именованные маршруты). Это также гарантирует, что страницы могут быть построены исключительно на основе маршрута без необходимости использовать контекст их родительской страницы.
Удачного вам кодинга!
Успеть на курс!
Umpiro
Как этот подход пересекается с provider? Относительные преимущества / недостатки?