1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-N8.md 15 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Оптимизация использования BuildContext в Flutter

Flutter предлагает концепцию BuildContext, с которой, вероятно, знаком каждый разработчик. Несмотря на то что она называется контекстом, на самом деле это абстракция объекта Element. В Flutter этот контекст обычно происходит от ComponentElement.

Для простоты можно разделить элементы Flutter на две категории:

  • RenderObjectElement: Элементы, имеющие RenderObject и способные выполнять отрисовку и макетирование.
  • ComponentElement: Элементы без RenderObject, такие как StatelessWidget и StatefulWidget, которые представляют собой StatelessElement и StatefulElement соответственно.

Поэтому, когда вы получаете BuildContext в методе build или в состоянии (State), вы фактически получаете ComponentElement.

Какие проблемы могут возникнуть при использовании BuildContext?

Пример кода показывает, что при нажатии кнопки FloatingActionButton производится задержка в два секунды перед тем, как вызвать pop для выхода из текущего экрана.

class _ControllerDemoPageState extends State<ControllerDemoPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          await Future.delayed(Duration(seconds: bk));
          Navigator.of(context).pop();
        },
      ),
    );
  }
}

Обычно такой подход не вызывает проблем, но если пользователь сразу после нажатия FloatingActionButton нажмёт AppBar для выхода из приложения, произойдет следующая ошибка.

Здесь заменил 2 на bk чтобы указать на необходимость исправления. В реальном примере должно быть Duration(seconds: 2) вместо Duration(seconds: bk).

Ошибка указывает, что соответствующий элемент уже удален, поскольку контекст был уничтожен вместе со всем приложением при вызове Navigator.of(context).

Чтобы решить эту проблему, достаточно проверять наличие mounted перед выполнением операции.

class _ControllerDemoPageState extends State<ControllerDemoPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          await Future.delayed(Duration(seconds: 2));
          if (!mounted) return;
          Navigator.of(context).pop();
        },
      ),
    );
  }
}

Конструкция кода внутри mounted берётся из State, потому что State зависит от Element и может воспринимать его жизненный цикл, например, mounted проверяет условие _element != null.

Итак, мы получили небольшой трюк: при использовании BuildContext в случае необходимости следует использовать mounted, чтобы гарантировать его действительность.

Но достаточно ли использования только mounted для удовлетворения требований оптимизации контекста?

Как показано ниже:

  • Мы добавляем список, используя builder для создания элементов
  • Каждый элемент списка имеет событие клика
  • При клике на элемент мы имитируем сетевой запрос, предположим, что сеть работает медленно, поэтому задержка составляет 5 секунд
  • После этого прокручиваем список так, чтобы кликнутый элемент вышел за пределы экрана```dart class _ControllerDemoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: ListView.builder( itemBuilder: (context, index) { return ListItem(); }, itemCount: 30, ), ); } }

class ListItem extends StatefulWidget { const ListItem({Key? key}) : super(key: key);

@override State createState() => _ListItemState(); }

class _ListItemState extends State { @override Widget build(BuildContext context) { return ListTile( title: Container( height: 160, color: Colors.amber, ), onTap: () async { await Future.delayed(Duration(seconds: Yöntemler 5)); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Сообщение"))); }, ); } }


Поскольку элемент исчезает за пределами экрана в течение 5 секунд, он фактически освобождается, а значит, благодаря проверке `mounted`, `SnackBar` не будет отображаться.

*А если требуется отобразить результат обратной связи при клике даже после того, как элемент был освобожден, как это можно реализовать?*

Мы знаем, что как `ScaffoldMessenger.of(context)`, так и `Navigator.of(context)` всё ещё используют `context` для поиска соответствующего `InheritedWidget`. Поэтому мы можем заранее получить этот объект.

Поэтому, как показано ниже, перед вызовом `Future.delayed` мы получаем объект `sm` через `ScaffoldMessenger.of(context);`. Даже если вы покидаете текущий список, `SnackBar` будет корректно отображен через 5 секунд.

```dart
class _ListItemState extends State<ListItem> {
  late final ScaffoldMessengerState sm;

  @override
  void initState() {
    super.initState();
    sm = ScaffoldMessenger.of(context);
  }
}
```  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Container(
        height: 160,
        color: Colors.amber,
      ),
      onTap: () async {
        await Future.delayed(Duration(seconds: 5));
        if (!mounted) return;
        sm.showSnackBar(SnackBar(content: Text("Подсказка")));
      },
    );
  }
}
```Почему при уничтожении страницы Snackbar всё ещё может нормально отображаться?

Это происходит потому что через `of(context);` получаем объект `ScaffoldMessenger`, который находится внутри `MaterialApp`. Поэтому даже если страница уничтожена, это не влияет на выполнение `Snackbar`.

Однако, если мы модифицируем пример следующим образом, вложив еще один `ScaffoldMessenger` внутрь `Scaffold`, то в этом случае при попытке получить доступ к `ScaffoldMessenger` через `ScaffoldMessenger.of(context)` будет использоваться `ScaffoldMessenger` текущей страницы.

```dart
class _ControllerDemoPageState extends State<ControllerDemoPage> {
  @override
  Widget build(BuildContext context) {
    return ScaffoldMessenger(
      child: Scaffold(
        appBar: AppBar(),
        body: ListView.builder(
          itemBuilder: (context, index) {
            return ListItem();
          },
          itemCount: 30,
        ),
      ),
    );
  }
}
``

В этом случае мы можем гарантировать, что `Snackbar` будет правильно отображаться, когда элемент невидим. Однако, если мы сразу выйдем с этой страницы, возникнет следующая ошибка, так как `ScaffoldMessenger` также был уничтожен.

![Ошибка](http://img.cdn.guoshuyu.cn/20220720_N8/image3.png)

Итак, здесь мы узнали второй небольшой трюк: **в асинхронных операциях использовать `of(context)` для раннего получения данных, чтобы обеспечить полное выполнение процесса**.А где лучше всего вызывать `of(context)`?

Не забудьте о логах, которые были показаны ранее? В момент ошибки первого примера, лог указывал на метод `didChangeDependencies` состояния.

![Лог](http://img.cdn.guoshuyu.cn/20220720_N8/image1.png)

Зачем официально рекомендуют вызывать `of(context)` именно в этом методе? Ранее мы говорили, что через `of(context)` получается `InheritedWidget`. Когда `InheritedWidget` изменяется, это приводит к вызову метода `didChangeDependencies` в связанном состоянии (`State`) элемента. **Поэтому вызов `of(context)` внутри `didChangeDependencies` имеет хорошую причинно-следственную связь**. > Для заинтересованных в этой теме можно почитать [Flutter хитрости с MediaQuery и оптимизация build](https://juejin.cn/post/7114098725600903175) и [Полное понимание State и Provider](https://juejin.cn/post/6844903866706706439#heading-5).

Можно ли вызвать это в методе `initState`?

Конечно нет, если вы попробуете вызвать метод, такой как `ScaffoldMessenger.of(context).showSnackBar`, прямо в `initState`, то вы получите следующее сообщение об ошибке.

![](http://img.cdn.guoshuyu.cn/20220720_N8/image4.png)

Это связано с тем, что Element проверяет текущее состояние `_StateLifecycle`. Если это состояние равно `_StateLifecycle.created` или `_StateLifecycle.defunct`, то есть во время выполнения `initState` и `dispose`, операция `of(context)` запрещена.

![](http://img.cdn.guoshuyu.cn/20220720_N8/image5.png)

> Операция `of(context)` указывает на `context.dependOnInheritedWidgetOfExactType`.

Конечно, если вы все же хотите вызвать эту операцию в `initState`, вы можете использовать `Future`:```dart
@override
void initState() {
  super.initState();
  Future(() {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("initState")));
  });
}

Проще говоря, поскольку Dart является однопоточным, Future внутри initState выполняется при следующей итерации цикла, поэтому он уже не находится в состоянии _StateLifecycle.created.

А если я просто вызову его в build?

Вызов в build возможен, хотя build может выполняться довольно часто. Однако операция of(context) фактически представляет собой получение объекта типа через ключ-значение в карте, поэтому она не оказывает значительного влияния на производительность.

На самом деле, то, что действительно влияет на производительность — это количество вызовов of(context) и логики после получения объекта, например, когда вы используете MediaQuery.of(context).size для получения размера экрана и затем выполняете сложные вычисления для позиционирования вашего компонента.

  @override
  Widget build(BuildContext context) {
    var size = MediaQuery.of(context).size;
    var padding = MediaQuery.of(context).padding;
    var width = size.width / 2;
    var height = size.width / size.height * (30 - padding.bottom);
    return Container(
      color: Colors.amber,
      width: width,
      height: height,
    );
  }
```Например, этот код может привести к тому, что при открытии клавиатуры, даже если страница ещё полностью не отобразится, ваш компонент будет постоянно пересчитывать свои размеры, что может привести к замедлению.> Подробное объяснение можно найти в статье [Flutter хаки: MediaQuery и оптимизация build  секреты, которых вы не знали](https://juejin.cn/post/7114098725600903175)Итак, мы получили ещё один небольшой трюк: **все операции с использованием `of(context)` лучше выполнять внутри метода `didChangeDependencies`**.

Наконец, сегодня я хотел поделиться некоторыми моментами и советами при работе с `BuildContext`. Если у вас есть вопросы по этой теме, пожалуйста, оставьте свои комментарии ниже.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master