BuildContext, которого уже нет
Если посмотреть на топ ошибок в Crashlytics почти любого Flutter-проекта, там будет эта. Формулировки разные, суть одна: код обратился к контексту виджета, который к этому моменту уже убрали из дерева.
Как это выглядит
Future<void> _save() async {
await repository.save(form.value);
Navigator.of(context).pop();
}
Между await и следующей строкой проходит время. За это время пользователь мог нажать «назад», система могла закрыть экран, родитель мог перестроиться и выбросить этот виджет. Когда управление возвращается, context указывает на элемент, которого в дереве больше нет.
Линтер это ловит, правило называется use_build_context_synchronously. Включите, если ещё нет:
linter:
rules:
- use_build_context_synchronously
Почему mounted недостаточно
Стандартный ответ — проверить mounted:
Future<void> _save() async {
await repository.save(form.value);
if (!mounted) return;
Navigator.of(context).pop();
}
Это правильно и в большинстве случаев достаточно. Но есть нюанс, о котором вспоминают редко: проверка обязана стоять после каждого await, а не один раз в начале. Вот так уже сломано:
if (!mounted) return;
await repository.save(form.value);
Navigator.of(context).pop(); // mounted проверен до await
И второй нюанс: в StatelessWidget никакого mounted нет. Там придётся либо тащить состояние выше, либо не держать контекст в асинхронном коде вообще.
Способ, который мне нравится больше
Не передавать контекст в асинхронный код. Захватите всё, что нужно, до await — а после используйте уже захваченные объекты:
Future<void> _save() async {
final navigator = Navigator.of(context);
final messenger = ScaffoldMessenger.of(context);
try {
await repository.save(form.value);
navigator.pop();
} on SaveFailure catch (e) {
messenger.showSnackBar(SnackBar(content: Text(e.message)));
}
}
NavigatorState и ScaffoldMessengerState переживают исчезновение вашего виджета — они принадлежат экранам выше по дереву. Поэтому вызов на них безопасен даже тогда, когда текущий экран уже уехал.
Побочный плюс: код становится честнее. Видно, какие именно зависимости от дерева нужны этому методу, и их можно подменить в тесте.
Отдельно про диалоги
Закрытие диалога чужим контекстом — источник особенно неприятных багов, потому что вместо краша вы получаете закрытие не того экрана. У showDialog билдер даёт собственный контекст, и для pop нужен именно он:
showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
content: const Text('Удалить черновик?'),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Отмена'),
),
],
),
);
Назовите параметр dialogContext, а не context. Тогда он не затенит внешний, и на ревью сразу видно, какой именно контекст используется.