Pull to refresh
758.77
OTUS
Цифровые навыки от ведущих экспертов

InheritedWidget во Flutter

Reading time5 min
Views20K
Original author: Mehmet Fidanboylu
Перевод статьи подготовлен для студентов курса «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 кодированием, если вы используете именованные маршруты). Это также гарантирует, что страницы могут быть построены исключительно на основе маршрута без необходимости использовать контекст их родительской страницы.

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

Успеть на курс!
Tags:
Hubs:
+11
Comments3

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS