Scaffold.of () wywołano z kontekstem, który nie zawiera Scaffold

183

Jak widzisz, mój przycisk znajduje się wewnątrz ciała Scaffolda. Ale mam ten wyjątek:

Scaffold.of () wywołano z kontekstem, który nie zawiera 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);
}

EDYTOWAĆ:

Znalazłem inne rozwiązanie tego problemu. Jeśli damy Scaffoldowi klucz, którym jest GlobalKey, możemy wyświetlić SnackBar jako następujący bez potrzeby zawijania naszego ciała widżetem Builder. Jednak widget, który zwraca Scaffold, powinien być widgetem Stateful .:

 _scaffoldKey.currentState.showSnackBar(snackbar); 
Figen Güngör
źródło
Znalazłem bardzo dobry samouczek, w którym zrobił opakowanie, aby mógł łatwo zmienić lub kontrolować kontekst całej aplikacji: noobieprogrammer.blogspot.com/2020/06/…
doppelgunner

Odpowiedzi:

245

Ten wyjątek występuje, ponieważ używasz contextwidgetu, którego wystąpienie zostało utworzone Scaffold. Nie contextdziecka Scaffold.

Możesz rozwiązać ten problem, używając po prostu innego kontekstu:

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

Zauważ, że chociaż Buildertutaj używamy , nie jest to jedyny sposób na uzyskanie innego BuildContext.

Możliwe jest również wyodrębnienie poddrzewa do innego Widget(zwykle za pomocą extract widgetrefaktora)

Rémi Rousselet
źródło
Otrzymuję ten wyjątek z tym: setState () lub markNeedsBuild () wywołane podczas kompilacji. I / flutter (21754): tego widżetu Scaffold nie można oznaczyć jako wymagającego kompilacji, ponieważ struktura jest już w I / flutter (21754): proces tworzenia widgetów.
Figen Güngör
1
onPressedZdałeś aby RaisedButtonnie jest funkcją. Zmień to na() => _displaySnackBar(context)
Rémi Rousselet
Gotcha: onPressed: () {_displaySnackBar (context);}, Thanks =)
Figen Güngör
1
Pomyślałem, że z .of (kontekst) ma podróżować w górę hierarchii widżetów, aż napotka jeden z podanych typów, w tym przypadku Scaffold, z którym powinien się spotkać zanim osiągnie „Budowanie widżetu (..”. Czy to nie działa w ten sposób?
CodeGrue
@ RémiRousselet, Ile typów kontekstów występuje we Flutterze? Jak powiedziałeś, kontekst widgetu, kontekst dziecka Scaffolda.
CopsOnRoad
121

Możesz użyć GlobalKey. Jedynym minusem jest to, że użycie GlobalKey może nie być najefektywniejszym sposobem zrobienia tego.

Dobrą rzeczą jest to, że możesz również przekazać ten klucz do innych niestandardowych klas widżetów, które nie zawierają żadnego szkieletu. Zobacz ( tutaj )

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
źródło
Dzięki Lebohang Mbele
programista
1
Czy mogę zapytać, jak to powtórzyć, gdy drzewo składa się z wielu widżetów zdefiniowanych w wielu plikach? _scaffoldKeyjest prywatne ze względu na wiodące podkreślenie. Ale nawet gdyby tak nie było, znajduje się wewnątrz HomePageklasy i dlatego jest niedostępny bez tworzenia wystąpienia nowej strony głównej gdzieś w drzewie, pozornie tworząc cykliczne odwołanie.
ExactaBox
1
aby odpowiedzieć na moje własne pytanie: utwórz klasę singletonową z GlobalKey<ScaffoldState>właściwością, edytuj konstruktora Widgeta z rusztowaniem, aby ustawić właściwość klucza singletona, a następnie w drzewie widgetu potomnego możemy wywołać coś takiego mySingleton.instance.key.currentState.showSnackBar()...
ExactaBox
Dlaczego jest to nieefektywne?
user2233706
@ user2233706 Jest to wymienione w dokumentacji GlobalKey tutaj . Również ten post może rzucić więcej światła.
Lebohang Mbele
43

Dwa sposoby rozwiązania tego problemu

1) Korzystanie z widżetu 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) Korzystanie z 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
źródło
16

Sprawdź to w dokumentacji metody:

Gdy szkielet jest faktycznie tworzony w tej samej funkcji budowania, argument kontekstowy funkcji budowania nie może zostać użyty do znalezienia szkieletu (ponieważ znajduje się on „nad” zwracanym widgetem). W takich przypadkach można użyć następującej techniki z narzędziem Builder, aby zapewnić nowy zakres z BuildContext znajdującym się „pod” szkieletem:

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

Możesz sprawdzić opis w dokumentacji metody

SANAT
źródło
13

Prostym sposobem rozwiązania tego problemu będzie utworzenie klucza do swojego rusztowania, takiego jak ten finał, z następującym kodem:

Pierwszy: GlobalKey<ScaffoldState>() _scaffoldKey = GlobalKey<ScaffoldState> ();

Sekunda: przypisz klucz do swojego rusztowania key: _scaffoldKey

Po trzecie: Zadzwoń do Snackbar za pomocą _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text("Welcome")));

Mohamed Abdul-Malik
źródło
3

To samo zachowanie, którego doświadczasz, jest nawet określane jako „trudny przypadek” w dokumentacji Fluttera .

Jak naprawić

Problem został rozwiązany na różne sposoby, jak widać na podstawie innych odpowiedzi zamieszczonych tutaj. Na przykład dokumentacja, do której się odwołuję, rozwiązuje problem za pomocą pliku Builderwhich

wewnętrzna, BuildContextaby onPressedmetody mogły odnosić się do Scaffoldwith Scaffold.of().

Tym samym sposobem rozmowy showSnackBarz rusztowań byłoby

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

Teraz trochę szczegółów dla ciekawskiego czytelnika

Sam uznałem za całkiem pouczające zapoznanie się z dokumentacją Fluttera po prostu ( Android Studio ) ustawiając kursor na fragmencie kodu ( klasa Flutter , metoda itp.) I naciskając ctrl + B, aby wyświetlić dokumentację dla tego konkretnego fragmentu.

Konkretny problem , z którym się spotykasz , jest opisany w dokumentacji BuildContext , gdzie można ją przeczytać

Każdy widget ma swój własny BuildContext , który staje się elementem nadrzędnym widgetu zwracanego przez funkcję [...]. Build.

Oznacza to więc, że w naszym przypadku kontekst będzie rodzicem naszego widgetu Scaffold po jego utworzeniu (!). Ponadto dokumentacja Scaffold.of mówi, że zwraca

Stan z najbliższej instancji [ Scaffold ] tej klasy, która zawiera podany kontekst.

Ale w naszym przypadku kontekst nie obejmuje (jeszcze) rusztowania (nie zostało jeszcze zbudowane). Tam właśnie wkracza Builder !

Po raz kolejny docu nas oświeca. Tam możemy czytać

[Klasa Builder jest po prostu] Platonicznym widgetem, który wywołuje zamknięcie w celu uzyskania widgetu potomnego.

Hej, chwileczkę, co !? Ok, przyznaję: to niewiele pomaga ... Ale wystarczy powiedzieć (po kolejnym wątku SO )

Celem klasy Builder jest po prostu tworzenie i zwracanie widgetów potomnych.

Więc teraz wszystko staje się jasne! Wywołując Builder wewnątrz Scaffolda , budujemy Scaffold, aby móc uzyskać jego własny kontekst, i uzbrojeni w ten internalContext możemy ostatecznie wywołać Scaffold.of (innerContext)

Poniżej znajduje się opisana wersja powyższego kodu

@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
źródło
1

Nie zawracałbym sobie głowy używaniem domyślnego paska przekąskowego, ponieważ możesz zaimportować pakiet flushbar, który zapewnia większą możliwość dostosowania:

https://pub.dev/packages/flushbar

Na przykład:

Flushbar(
                  title:  "Hey Ninja",
                  message:  "Lorem Ipsum is simply dummy text of the printing and typesetting industry",
                  duration:  Duration(seconds: 3),              
                )..show(context);
Dataanonymous
źródło
0

Bardziej wydajnym rozwiązaniem jest podzielenie funkcji kompilacji na kilka widżetów. Wprowadza to „nowy kontekst”, z którego można uzyskać Scaffold

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
źródło
0

Wyodrębnij widżet przycisku, który wyświetli pasek z przekąskami.

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'),
                  );
  }
}
Rashid
źródło
Jak wypada Twoje podejście do istniejących odpowiedzi? Czy istnieje powód, aby wybrać to podejście zamiast, powiedzmy, zaakceptowanej odpowiedzi?
Jeremy Caney
0

tutaj używamy kreatora do zawijania innego widżetu, w którym potrzebujemy paska przekąskowego

Builder(builder: (context) => GestureDetector(
    onTap: () {
        Scaffold.of(context).showSnackBar(SnackBar(
            content: Text('Your Services have been successfully created Snackbar'),
        ));
        
    },
    child: Container(...)))
Krishna Caitanya
źródło