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
для создания элементов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` также был уничтожен.

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

Зачем официально рекомендуют вызывать `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`, то вы получите следующее сообщение об ошибке.

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

> Операция `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 )