Scaffold.of () викликається з контекстом, який не містить Scaffold


184

Як ви бачите, моя кнопка знаходиться в тілі скелі. Але я отримую такий виняток:

Scaffold.of () викликається з контекстом, який не містить Scaffold.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SnackBar Playground'),
      ),
      body: Center(
        child: RaisedButton(
          color: Colors.pink,
          textColor: Colors.white,
          onPressed: _displaySnackBar(context),
          child: Text('Display SnackBar'),
        ),
      ),
    );
  }
}

_displaySnackBar(BuildContext context) {
  final snackBar = SnackBar(content: Text('Are you talkin\' to me?'));
  Scaffold.of(context).showSnackBar(snackBar);
}

Редагувати:

Я знайшов інше рішення цієї проблеми. Якщо ми дамо Scaffold ключ, який є GlobalKey, ми можемо відобразити SnackBar наступним чином без необхідності обертати наше тіло віджетом Builder. Віджет, який повертає ліси, повинен бути віджетом Stateful:

 _scaffoldKey.currentState.showSnackBar(snackbar); 

Я знайшов дуже хороший підручник, де він зробив обгортку, щоб він міг легко змінити чи контролювати контекст усього додатка: noobieprogrammer.blogspot.com/2020/06/…
doppelgunner

Відповіді:


245

Цей виняток трапляється тому, що ви використовуєте contextінший файл віджета Scaffold. Не contextє дитиною Scaffold.

Вирішити це можна, використовуючи інший контекст:

Scaffold(
    appBar: AppBar(
        title: Text('SnackBar Playground'),
    ),
    body: Builder(
        builder: (context) => 
            Center(
            child: RaisedButton(
            color: Colors.pink,
            textColor: Colors.white,
            onPressed: () => _displaySnackBar(context),
            child: Text('Display SnackBar'),
            ),
        ),
    ),
);

Зауважте, що хоча ми Builderтут використовуємо , це не єдиний спосіб отримати інший BuildContext.

Також можливо витягнути підребро в інше Widget(як правило, використовуючи extract widgetрефактор)


Я отримую цей виняток завдяки цьому: setState () або markNeedsBuild () викликається під час збирання. I / flutter (21754): Цей віджет Scaffold не може бути позначений як необхідний для створення, оскільки рамка вже знаходиться в I / flutter (21754): процес створення віджетів.
Figen Güngör

1
Переданий onPressedтобі RaisedButtonне є функцією. Змініть його на() => _displaySnackBar(context)
Ремі Русселет

Отримав: onПрессував: () {_displaySnackBar (контекст);}, спасибі =)
Figen Güngör

1
Я думав, що з .of (контексту) це повинно подорожувати вгору до ієрархії віджетів, поки він не зіткнеться з одним із заданих типів, в даному випадку Scaffold, з яким він повинен зіткнутися, перш ніж досягти "Збірка віджетів (..". Чи це не працює таким чином?
CodeGrue

@ RémiRousselet, Скільки типів контексту існує у Flutter? Як ви сказали, контекст віджета, контекст дитини Ешафот.
CopsOnRoad

121

Ви можете використовувати a GlobalKey. Єдиним недоліком є ​​те, що використання GlobalKey може бути не найефективнішим способом цього зробити.

Хороша річ у тому, що ви також можете передати цей ключ іншому користувальницькому класу віджетів, які не містять ешафот. Дивіться ( тут )

class HomePage extends StatelessWidget {
  final _scaffoldKey = GlobalKey<ScaffoldState>(); \\ new line
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,                           \\ new line
      appBar: AppBar(
        title: Text('SnackBar Playground'),
      ),
      body: Center(
        child: RaisedButton(
          color: Colors.pink,
          textColor: Colors.white,
          onPressed: _displaySnackBar(context),
          child: Text('Display SnackBar'),
        ),
      ),
    );
  }
  _displaySnackBar(BuildContext context) {
    final snackBar = SnackBar(content: Text('Are you talkin\' to me?'));
    _scaffoldKey.currentState.showSnackBar(snackBar);   \\ edited line
  }
}

Спасибі Lebohang Mbele
розробник

1
Чи можу я запитати, як повторити це, коли ваше дерево складається з декількох віджетів, визначених у кількох файлах? _scaffoldKeyє приватним через провідне підкреслення. Але навіть якщо цього не було, він знаходиться всередині HomePageкласу і тому недоступний, не створюючи нову сторінку HomePage десь під деревом, здавалося б, створюючи циркулярну посилання.
ExactaBox

1
щоб відповісти на моє власне запитання: створіть клас одиночки з GlobalKey<ScaffoldState>властивістю, відредагуйте конструктор віджету за допомогою ешафот, щоб встановити ключову властивість mySingleton.instance.key.currentState.showSnackBar()
сингтона

Чому це неефективно?
користувач2233706

@ user2233706 Про це йдеться в документації GlobalKey тут . Також ця публікація може пролити більше світла.
Lebohang Mbele

43

Два способи вирішення цієї проблеми

1) Використання віджета Builder

Scaffold(
    appBar: AppBar(
        title: Text('My Profile'),
    ),
    body: Builder(
        builder: (ctx) => RaisedButton(
            textColor: Colors.red,
            child: Text('Submit'),
            onPressed: () {
                 Scaffold.of(ctx).showSnackBar(SnackBar(content: Text('Profile Save'),),);
            }               
        ),
    ),
);

2) Використання GlobalKey

class HomePage extends StatelessWidget {

  final globalKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
     return Scaffold(
       key: globalKey,
       appBar: AppBar(
          title: Text('My Profile'),
       ),
       body:  RaisedButton(
          textColor: Colors.red,
          child: Text('Submit'),
          onPressed: (){
               final snackBar = SnackBar(content: Text('Profile saved'));
               globalKey.currentState.showSnackBar(snackBar);
          },
        ),
     );
   }
}

16

Перевірте це в документації методу:

Коли Scaffold фактично створений у тій же функції збірки, аргумент контексту функції збирання не може бути використаний для пошуку Scaffold (оскільки він "вище" віджета, який повертається). У таких випадках наступна методика з Builder може бути використана для надання нового обсягу з BuildContext, який знаходиться "під" ешафотом:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Demo')
    ),
    body: Builder(
      // Create an inner BuildContext so that the onPressed methods
      // can refer to the Scaffold with Scaffold.of().
      builder: (BuildContext context) {
        return Center(
          child: RaisedButton(
            child: Text('SHOW A SNACKBAR'),
            onPressed: () {
              Scaffold.of(context).showSnackBar(SnackBar(
                content: Text('Hello!'),
              ));
            },
          ),
        );
      },
    ),
  );
}

Ви можете перевірити опис у документах методу


13

Простим способом вирішити цю проблему буде створення ключа для вашого ешафота, як цей фінал, із наступним кодом:

Перший: GlobalKey<ScaffoldState>() _scaffoldKey = GlobalKey<ScaffoldState> ();

Екран: Призначте ключ вашому лісу key: _scaffoldKey

По-третє: зателефонуйте на закуску за допомогою _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text("Welcome")));


3

Сама поведінка, яку ви переживаєте, навіть називається «хитрою справою» у документації Flutter .

Як виправити

Питання вирішується по-різному, як видно з інших відповідей, розміщених тут. Наприклад, частина документації, яку я посилаюсь, вирішує проблему, використовуючи, Builderщо створює

внутрішній, BuildContextщоб onPressedметоди могли посилатися на Scaffoldс Scaffold.of().

Таким чином, спосіб зателефонувати showSnackBarз ешафотів був би

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text('Demo')),
    body: Builder(
      builder: (BuildContext innerContext) {
        return FlatButton(
          child: Text('BUTTON'),
          onPressed: () {
            Scaffold.of(innerContext).showSnackBar(SnackBar(
              content: Text('Hello.')
            ));
          }
        );
      }
    )
  );
}

Тепер трохи деталей для допитливого читача

Я сам знайшов дуже повчальний вивчити флатер документації просто ( Android Studio ) встановивши курсор на шматку коду ( Flutter класу, методу і т.д.) і натиснувши Ctrl + B , щоб показати документацію для цієї конкретної частини.

Конкретна проблема, з якою ви стикаєтеся , згадується в документі для BuildContext , де її можна прочитати

Кожен віджет має власний BuildContext , який стає батьківським віджетом, повернутим функцією [...]. Build.

Отже, це означає, що в нашому випадку контекст буде батьком нашого віджета Scaffold, коли він створений (!). Далі, документ для Scaffold.of каже, що він повертається

Стан від найближчого [ Scaffold ] екземпляра цього класу, який закриває заданий контекст.

Але в нашому випадку контекст не охоплює (ще) ешафот (він ще не побудований). Там де Builder вступає в дію!

Ще раз документ висвітлює нас. Там ми можемо читати

[Клас Builder, просто] Платонічний віджет, який викликає закриття, щоб отримати його дочірній віджет.

Гей, почекай хвилинку, що !? Гаразд, я визнаю: це не дуже допомагає ... Але досить сказати (слідуючи за іншою темою SO ), що

Мета класу Builder - просто створити та повернути дочірні віджети.

Тож тепер усе стає зрозуміло! Закликаючи Builder всередині Scaffold, ми будуємо Scaffold для того, щоб отримати власний контекст, і, озброївшись тим внутрішнім контекстом, ми можемо нарешті назвати Scaffold.of (innerContext)

Далі йде анотована версія коду

@override
Widget build(BuildContext context) {
  // here, Scaffold.of(context) returns null
  return Scaffold(
    appBar: AppBar(title: Text('Demo')),
    body: Builder(
      builder: (BuildContext innerContext) {
        return FlatButton(
          child: Text('BUTTON'),
          onPressed: () {
            // here, Scaffold.of(innerContext) returns the locally created Scaffold
            Scaffold.of(innerContext).showSnackBar(SnackBar(
              content: Text('Hello.')
            ));
          }
        );
      }
    )
  );
}

1

Я б не заважав використовувати закусочну за замовчуванням, тому що ви можете імпортувати пакет флеш-панелі, що забезпечує більшу налаштованість:

https://pub.dev/packages/flushbar

Наприклад:

Flushbar(
                  title:  "Hey Ninja",
                  message:  "Lorem Ipsum is simply dummy text of the printing and typesetting industry",
                  duration:  Duration(seconds: 3),              
                )..show(context);

0

Більш ефективне рішення - розділити функцію збирання на кілька віджетів. Це вводить "новий контекст", з якого можна отримати ешафот

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Scaffold.of example.')),
        body: MyScaffoldBody(),
      ),
    );
  }
}

class MyScaffoldBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: RaisedButton(
          child: Text('Show a snackBar'),
          onPressed: () {
            Scaffold.of(context).showSnackBar(
              SnackBar(
                content: Text('Have a Snack'),
              ),
            );
          }),
    );
  }
}

0

Витягніть свій віджет із кнопками, який відобразить закусочну.

class UsellesslyNestedButton extends StatelessWidget {
  const UsellesslyNestedButton({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: (){
        showDefaultSnackbar(context);
                    },
                    color: Colors.blue,
                    child: Text('Show about'),
                  );
  }
}

Як ваш підхід порівнюється з існуючими відповідями? Чи є причина обрати такий підхід через, скажімо, прийняту відповідь?
Джеремі Кейні

0

тут ми використовуємо конструктор, щоб загорнути інший віджет, де нам потрібна закусочна

Builder(builder: (context) => GestureDetector(
    onTap: () {
        Scaffold.of(context).showSnackBar(SnackBar(
            content: Text('Your Services have been successfully created Snackbar'),
        ));
        
    },
    child: Container(...)))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.