Flutter i snackbar. 3 sposoby na rozwiązanie problemu z BuildContext

Flutter. W tym artykule dowiesz się w jaki sposób poradzić sobie z wyjątkiem "Scaffold.of() called with a context that does not contain a Scaffold."

Flutter i snackbar. 3 sposoby na rozwiązanie problemu z BuildContext

Tworzysz aplikację Flutter. Co zrobić, gdy podczas próby wyświetlenia snackbara otrzymujemy następujący exception?

Scaffold.of() called with a context that does not contain a Scaffold.

Gdy po raz pierwszy natknąłem się na ten problem spędziłem kilka konkretnych godzin zanim świadomie zrozumiałem mój błąd. A wiec kiedy się on pojawia? Zgodnie z opisem wyjątku wtedy gdy próbujemy uzyskać instancję Scaffold na podstawie contextu, który go nie zawiera. Dzieje się tak, gdy wykorzystywany kontekst pochodzi z tego samego widgetu StatefulWidget, co ten, którego funkcja kompilacji tworzy widget Scaffold.

Proste, ale namierzenie miejsca występowania problemu za pierwszym razem może nastręczyć nie lada trudności.

😇
Dzieje się tak, gdy wykorzystywany kontekst pochodzi z tego samego widgetu StatefulWidget, co ten, którego funkcja kompilacji tworzy widget Scaffold.

Flutter i snackbar. Przykładowy kod

Przeanalizuj poniższy kod. Jeśli spróbowałbyś wcisnąć przycisk, który według opisu powinien wyświetlić snackbara, uzyskasz omawiany wyjątek.

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Click below to show a snackbar!',
            ),
            OutlineButton(
              child: Text("Click!"),
              onPressed: () {
                final snackBar = SnackBar(content: Text('My SnackBar!'));

                Scaffold.of(context).showSnackBar(snackBar);
              },
            )
          ],
        ),
      ),
    );
  }
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      home: MyHomePage(),
    );
  }
}

W czym problem? Przecież widok jest zawarty w obiekcie Scaffold, więc wszystko powinno być ok. To prawda, o ile w danym momencie on już istnieje.

No dobrze, problem polega na tym, że w momencie wywołania Scaffold.of(context), Flutter nie zbudował jeszcze w pełni drzewa widoku. To dlatego pomimo umieszczenia jako rodzica właśnie obiektu Scaffold, otrzymujemy exception podczas próby pokazania snackbara.

W takim razie jak to naprawić?

Znam 3 sposoby na poprawę tego problemu. Wybór rozwiązania należy dla Ciebie.

1. Skorzystanie z widgetu Builder.

Zanim jego funkcja przypisana do builder zostanie wywołana jego rodzice muszą zostać zbudowani. Dzięki czemu wywołanie Scaffold.of(context), będzie w stanie wyszukać Scaffold w drzewie widgetów, ponieważ został już istnieje. To pozwoli na poprawne wyświetlenie snackbara. Parametr w currentContext równie dobrze mógłby nazywać się po prostu context.

lass MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Builder(
        builder: (currentContext) => Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(
                'Click below to show a snackbar!',
              ),
              OutlineButton(
                child: Text("Click!"),
                onPressed: () {
                  final snackBar = SnackBar(content: Text('My SnackBar!'));

                  Scaffold.of(currentContext).showSnackBar(snackBar);
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      home: MyHomePage(),
    );
  }
}
2. Nowy widget

Problem zostanie rozwiązany podobnie jak wcześniej. W momencie wywołania snackbara skorzystamy z nowego contextu, który będzie w stanie odnaleźć instancję Scaffold.

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Demo"),
        ),
        body: MyContent());
  }
}

class MyContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(
          'Click below to show a snackbar!',
        ),
        OutlineButton(
          child: Text("Click!"),
          onPressed: () {
            final snackBar = SnackBar(content: Text('My SnackBar!'));

            Scaffold.of(context).showSnackBar(snackBar);
          },
        )
      ],
    ));
  }
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      home: MyHomePage(),
    );
  }
}
3. Skorzystanie z GlobalKey

Moim zdaniem najlepsze rozwiązanie. W tym rozwiązaniu odwołujemy się już bezpośrednio do obiektu Scaffold, dlatego samo wywołanie snackbara jest inne niż poprzednio.

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  var scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: scaffoldKey,
      appBar: AppBar(
        title: Text("Demo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Click below to show a snackbar!',
            ),
            OutlineButton(
              child: Text("Click!"),
              onPressed: () {
                final snackBar = SnackBar(content: Text('My SnackBar!'));

                scaffoldKey.currentState.showSnackBar(snackBar);
              },
            )
          ],
        ),
      ),
    );
  }
}

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      home: MyHomePage(),
    );
  }
}

Podsumowanie

Mam nadzieję, że wyjaśniłem Ci, co jest przyczyną problemu, a także to, w jaki sposób sobie z nim poradzić. Przedstawiłem Ci trzy rozwiązania, które możesz użyć w swojej aplikacji Flutter, a Ty wybierz te, które najlepiej sprawdzi się w konkretnym przypadku.

of method - Scaffold class - material library - Dart API
API docs for the of method from the Scaffold class, for the Dart programming language.
BuildContext class - widgets library - Dart API
API docs for the BuildContext class from the widgets library, for the Dart programming language.