Scaffold.of () вызывается с контекстом, который не содержит Scaffold

188

Как вы можете видеть, моя кнопка находится внутри корпуса Скаффолда. Но я получаю это исключение:

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. Хотя виджет, который возвращает Scaffold, должен быть виджетом Stateful.

 _scaffoldKey.currentState.showSnackBar(snackbar); 
Figen Güngör
источник
Я нашел очень хороший учебник, где он сделал оболочку, чтобы он мог легко изменять или контролировать контекст всего приложения: noobieprogrammer.blogspot.com/2020/06/…
doppelgunner

Ответы:

249

Это исключение происходит потому, что вы используете 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)
Rémi Rousselet
Получил: onPressed: () {_displaySnackBar (context);}, спасибо =)
Figen Güngör
1
Я подумал, что с .of (context) он должен перемещаться вверх по иерархии виджетов, пока не встретит один из заданного типа, в данном случае, Scaffold, с которым он должен столкнуться, прежде чем достигнет «Сборка виджета (..». Не работает таким образом?
CodeGrue
@ RémiRousselet, сколько типов контекста есть во флаттере? Как вы сказали, контекст виджета, контекст дочернего элемента скаффолда.
CopsOnRoad
121

Вы можете использовать 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()...
ExactaBox
Почему это неэффективно?
user2233706
@ user2233706 Это упоминается в документации GlobalKey здесь . Также этот пост может пролить больше света.
Лебоханг Мбеле
44

Два способа решить эту проблему

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);
          },
        ),
     );
   }
}
Sanjayrajsinh
источник
16

Проверьте это из документации метода:

Когда Scaffold фактически создается в той же функции сборки, контекстный аргумент функции build не может использоваться для поиска Scaffold (так как он «выше» возвращаемого виджета). В таких случаях для создания новой области действия с BuildContext, который находится «под» скаффолдом, можно использовать следующую технику с помощью Builder:

@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!'),
              ));
            },
          ),
        );
      },
    ),
  );
}

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

SANAT
источник
13

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

Первый: GlobalKey<ScaffoldState>() _scaffoldKey = GlobalKey<ScaffoldState> ();

Scecond: назначьте ключ вашему эшафоту 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.')
            ));
          }
        );
      }
    )
  );
}

Теперь немного подробностей для любознательного читателя

Я сам нашел весьма поучительным изучить документацию Flutter , просто ( Android Studio ) установив курсор на фрагмент кода ( класс Flutter , метод и т. Д.) И нажав ctrl + B, чтобы показать документацию для этого конкретного фрагмента.

Конкретная проблема, с которой вы сталкиваетесь, упоминается в документе для BuildContext , где можно прочитать

Каждый виджет имеет свой собственный BuildContext , который становится родителем виджета, возвращаемого функцией [...]. Build.

Таким образом, это означает, что в нашем случае контекст будет родителем нашего виджета Scaffold при его создании (!). Далее, документ для Scaffold.of говорит, что он возвращает

Состояние из ближайшего экземпляра [ Scaffold ] этого класса, охватывающего данный контекст.

Но в нашем случае контекст не включает (пока) Скаффолд (он еще не построен). Там, где Строитель вступает в действие!

Еще раз, документ освещает нас. Там мы можем прочитать

[Класс Builder - это просто] Платонический виджет, который вызывает замыкание для получения дочернего виджета.

Эй, подожди минутку, что? Хорошо, я признаю: это не очень помогает ... Но достаточно сказать (следуя другому такому потоку ), что

Цель класса Builder - просто создавать и возвращать дочерние виджеты.

Так что теперь все становится ясно! Вызывая Builder внутри Scaffold, мы строим Scaffold для того, чтобы иметь возможность получить его собственный контекст, и, вооружившись этим innerContext, мы можем наконец вызвать 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.')
            ));
          }
        );
      }
    )
  );
}
Deczaloth
источник
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);
Datanonymous
источник
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'),
              ),
            );
          }),
    );
  }
}
TDM
источник
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(...)))
Кришна Чайтанья
источник