Урок засвоєно: спільний контекст для діалогів у Flutter

tl;dr: Для обробки ProviderNotFoundException в Dialog потрібно повторно впровадити ChangeNotifier у контекст діалогу за допомогою ChangeNotifierProvider.value.

Іноді баги, з якими ми стикаємось, відкривають нам можливість глибше зрозуміти, як працюють інструменти, якими ми користуємось, і один з таких багів, з яким ми нещодавно зіткнулись, стосується доступу до ChangeNotifier в Dialog.

Отже, що саме таке ChangeNotifier? Це ключовий компонент пакету provider у Flutter, який використовується для керування станом. Фактично, ChangeNotifier — це об'єкт, до якого можна підключити прослуховувачів, що працює за принципом, схожим на патерн Observer.

При спробі доступу до ChangeNotifier всередині Dialog зазвичай виникає ProviderNotFoundException. Щоб зрозуміти цю проблему і дізнатися, як її вирішити, давайте розглянемо практичний приклад.

Уявімо собі простий ChangeNotifier, RocketLauncher, який ініціює запуск ракети, що може бути як успішним, так і невдалим:

class RocketLauncher extends ChangeNotifier {  
 int failed = 0;  
 int successful = 0;
int get count => failed + successful; void launch() {  
 if (Random().nextBool()) {  
 successful += 1;  
 } else {  
 failed += 1;  
 }  
 notifyListeners();  
 }  
}

Цей ChangeNotifier повідомляє своїх слухачів щоразу, коли відбувається запуск.

Структура нашого додатку виглядає так:

class MyApp extends StatelessWidget {  
 const MyApp({super.key});
@override  
 Widget build(BuildContext context) {  
 return MaterialApp(  
 home: ChangeNotifierProvider(  
 create: (context) => RocketLauncher(),  
 builder: (context, _) => const Scaffold(  
 body: Column(  
 mainAxisAlignment: MainAxisAlignment.center,  
 children: [  
 LaunchButton(),  
 StatsButton(),  
 ],  
 ),  
 ),  
 ),  
 );  
 }

Тут LaunchButton ініціює запуски, а StatsButton відображає статистику. ChangeNotifierProvider робить RocketLauncher доступним для нащадків віджетів.

class LaunchButton extends StatelessWidget {  
 const LaunchButton({super.key});
@override  
 Widget build(BuildContext context) {  
 return IconButton(  
 onPressed: context.watch<RocketLauncher>().launch,  
 icon: const Icon(Icons.rocket_launch),  
 );  
 }  
}

LaunchButton просто викликає метод launch у RocketLauncher.

class StatsButton extends StatelessWidget {  
 const StatsButton({super.key});
@override  
 Widget build(BuildContext context) {  
 final launcher = context.watch<RocketLauncher>(); return TextButton(  
 child: Text("${launcher.count} launches"),  
 onPressed: () {  
 showDialog(  
 context: context,  
 builder: (context) => const StatsDialog(),  
 );  
 },  
 );  
 }  
}

StatsButton показує загальну кількість запусків і відкриває діалог для додаткових деталей.

class StatsDialog extends StatelessWidget {  
 const StatsDialog({super.key});
@override  
 Widget build(BuildContext context) {  
 final launcher = context.read<RocketLauncher>(); return Dialog(  
 child: Column(  
 children: [  
 Text("${launcher.failed} failed rocket launches"),  
 Text("${launcher.successful} successful rocket launches"),  
 ],  
 ),  
 );  
 }  
}

StatsDialog читає RocketLauncher, щоб відобразити кількість невдалих і успішних запусків.

Однак, при відкритті діалогу виникає ця помилка:

The following ProviderNotFoundException was thrown building StatsDialog(dirty):  
Error: Could not find the correct Provider above this StatsDialog Widget  
This happens because you used a `BuildContext` that does not include the provider  
of your choice.

Причина виникнення цієї помилки полягає в тому, що showDialog у Flutter використовує інший контекст, ніж контекст його викликача.
Документація Flutter для showDialog вказує: “Віджет, який повертає builder, не має спільного контексту з тим місцем, з якого спочатку викликається showDialog.”

Щоб вирішити цю проблему, ми можемо передати існуючий екземпляр RocketLauncher в контекст діалогу, використовуючи ChangeNotifierProvider.value:

class StatsButton extends StatelessWidget {  
 const StatsButton({super.key});
@override  
 Widget build(BuildContext context) {  
 final launcher = context.watch<RocketLauncher>(); return TextButton(  
 child: Text("${launcher.count} launches"),  
 onPressed: () {  
 showDialog(  
 context: context,  
 builder: (context) => ChangeNotifierProvider.value(  
 value: launcher,  
 builder: (context, _) => const StatsDialog(),  
 ),  
 );  
 },  
 );  
 }  
}

На завершення, робота з ProviderNotFoundException у Flutter — це поширена проблема, яка підкреслює важливість розуміння керування контекстом у фреймворку Flutter. Використовуючи ChangeNotifierProvider.value, розробники можуть ефективно налагодити контекст між батьківськими віджетами та їх діалогами.

Перекладено з: Lesson learned: sharing context with Flutter dialogs

Leave a Reply

Your email address will not be published. Required fields are marked *