InheritedWidget во Flutter

Перевод статьи подготовлен для студентов курса «Flutter Mobile Developer».


Корни деревьев виджетов во 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 кодированием, если вы используете именованные маршруты). Это также гарантирует, что страницы могут быть построены исключительно на основе маршрута без необходимости использовать контекст их родительской страницы.

Удачного вам кодинга!

Успеть на курс!

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *