webentwicklung-frage-antwort-db.com.de

Flutter Provider setState () oder markNeedsBuild () werden während des Builds aufgerufen

Ich möchte eine Liste von Ereignissen laden und beim Abrufen von Daten eine Ladeanzeige anzeigen.

Ich versuche Provider Muster (tatsächlich eine vorhandene Anwendung umgestalten).

Die Anzeige der Ereignisliste hängt also von einem Status ab, der im Anbieter verwaltet wird.

Problem ist, wenn ich notifyListeners() zu schnell aufrufe, bekomme ich diese Ausnahme:

════════ Ausnahme von Stiftungsbibliothek abgefangen ════════

Die folgende Zusicherung wurde beim Versenden von Benachrichtigungen für EventProvider ausgelöst:

setState () oder markNeedsBuild () werden während des Builds aufgerufen.

...

Die Benachrichtigung zum Senden des EventProviders lautete: Instanz von 'EventProvider'

════════════════════════════════════════

Warten Sie einige Millisekunden, bevor Sie notifyListeners() aufrufen, um das Problem zu lösen (siehe kommentierte Zeile in der Provider-Klasse unten).

Dies ist ein einfaches Beispiel, das auf meinem Code basiert (hoffentlich nicht zu stark vereinfacht):

Hauptfunktion:

Future<void> main() async {
    runApp(
        MultiProvider(
            providers: [
                ChangeNotifierProvider(create: (_) => LoginProvider()),
                ChangeNotifierProvider(create: (_) => EventProvider()),
            ],
            child: MyApp(),
        ),
    );
}

Root-Widget:

class MyApp extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
        final LoginProvider _loginProvider = Provider.of<LoginProvider>(context, listen: true);
        final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: false);

        // load user events when user is logged
        if (_loginProvider.loggedUser != null) {
            _eventProvider.fetchEvents(_loginProvider.loggedUser.email);
        }

        return MaterialApp(
            home: switch (_loginProvider.status) { 
                case AuthStatus.Unauthenticated:
                    return MyLoginPage();
                case AuthStatus.Authenticated:
                    return MyHomePage();
            },
        );

    }
}

Homepage:

class MyHomePage extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
        final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: true);

        return Scaffold(
            body: _eventProvider.status == EventLoadingStatus.Loading ? CircularProgressIndicator() : ListView.builder(...)
        )
    }
}

Eventanbieter:

enum EventLoadingStatus { NotLoaded, Loading, Loaded }

class EventProvider extends ChangeNotifier {

    final List<Event> _events = [];
    EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.NotLoaded;

    EventLoadingStatus get status => _eventLoadingStatus;

    Future<void> fetchEvents(String email) async {
        //await Future.delayed(const Duration(milliseconds: 100), (){});
        _eventLoadingStatus = EventLoadingStatus.Loading;
        notifyListeners();
        List<Event> events = await EventService().getEventsByUser(email);
        _events.clear();
        _events.addAll(events);
        _eventLoadingStatus = EventLoadingStatus.Loaded;
        notifyListeners();
    }
}

Kann jemand erklären, was passiert?

4
Yann39

Sie rufen fetchEvents aus Ihrem Build-Code für das Root-Widget auf. Innerhalb von fetchEvents rufen Sie notifyListeners auf, das unter anderem setState für Widgets aufruft, die den Ereignisanbieter abhören. Dies ist ein Problem, da Sie setState in einem Widget nicht aufrufen können, wenn sich das Widget gerade in der Neuerstellung befindet.

An diesem Punkt denken Sie vielleicht, "aber die Methode fetchEvents ist als async markiert, sodass sie für später asynchron ausgeführt werden sollte". Und die Antwort darauf lautet "Ja und Nein". Die Funktionsweise von async in Dart besteht darin, dass Dart beim Aufrufen einer async -Methode versucht, so viel Code wie möglich synchron in der Methode auszuführen. Kurz gesagt bedeutet dies, dass jeder Code in Ihrer async -Methode, der vor einem await steht, als normaler synchroner Code ausgeführt wird. Wenn wir uns Ihre fetchEvents Methode ansehen:

Future<void> fetchEvents(String email) async {
  //await Future.delayed(const Duration(milliseconds: 100), (){});

  _eventLoadingStatus = EventLoadingStatus.Loading;

  notifyListeners();

  List<Event> events = await EventService().getEventsByUser(email);
  _events.clear();
  _events.addAll(events);
  _eventLoadingStatus = EventLoadingStatus.Loaded;

  notifyListeners();
}

Wir können sehen, dass das erste await beim Aufruf von EventService().getEventsByUser(email) passiert. Davor gibt es ein notifyListeners, das also synchron aufgerufen wird. Das bedeutet, dass das Aufrufen dieser Methode über die Erstellungsmethode eines Widgets so aussieht, als hätten Sie notifyListeners in der Erstellungsmethode selbst aufgerufen, was, wie gesagt, verboten ist.

Der Grund, warum es funktioniert, wenn Sie den Aufruf zu Future.delayed Hinzufügen, ist, dass sich jetzt oben in der Methode ein await befindet, wodurch alles darunter asynchron ausgeführt wird. Sobald die Ausführung den Teil des Codes erreicht hat, der notifyListeners aufruft, befindet sich Flutter nicht mehr in der Lage, Widgets neu zu erstellen. Daher ist es sicher, diese Methode zu diesem Zeitpunkt aufzurufen.

Sie können stattdessen fetchEvents über die Methode initState aufrufen, dies führt jedoch zu einem ähnlichen Problem: Sie können setState auch nicht aufrufen, bevor das Widget initialisiert wurde.

Die Lösung ist also diese. Anstatt alle Widgets zu benachrichtigen, die dem Ereignisanbieter zuhören, dass er geladen wird, muss er beim Erstellen standardmäßig geladen werden. (Dies ist in Ordnung, da nach dem Erstellen zunächst alle Ereignisse geladen werden. Es sollte also niemals ein Szenario geben, in dem es beim erstmaligen Erstellen nicht geladen werden muss.) Dadurch muss der Anbieter nicht mehr als markiert werden Laden zu Beginn der Methode, wodurch der Aufruf von notifyListeners dort entfällt:

EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.Loading;

...

Future<void> fetchEvents(String email) async {
  List<Event> events = await EventService().getEventsByUser(email);
  _events.clear();
  _events.addAll(events);
  _eventLoadingStatus = EventLoadingStatus.Loaded;

  notifyListeners();
}
11
Abion47

Das Problem ist, dass Sie notifyListeners zweimal in einer Funktion aufrufen. Ich verstehe, du willst den Zustand ändern. Es sollte jedoch nicht in der Verantwortung des EventProviders liegen, die App beim Laden zu benachrichtigen. Alles, was Sie tun müssen, ist, wenn es nicht geladen ist, anzunehmen, dass es geladen wird, und einfach ein CircularProgressIndicator einzugeben. Rufen Sie notifyListeners nicht zweimal in derselben Funktion auf, es nützt Ihnen nichts.

Wenn Sie es wirklich wollen, versuchen Sie Folgendes:

Future<void> fetchEvents(String email) async {
    markAsLoading();
    List<Event> events = await EventService().getEventsByUser(email);
    _events.clear();
    _events.addAll(events);
    _eventLoadingStatus = EventLoadingStatus.Loaded;
    notifyListeners();
}

void markAsLoading() {
    _eventLoadingStatus = EventLoadingStatus.Loading;
    notifyListeners();
}
0
Benjamin