web-dev-qa-db-fra.com

Flutter Provider setState () ou markNeedsBuild () appelé pendant la génération

Je souhaite charger une liste d'événements et afficher un indicateur de chargement lors de la récupération des données.

J'essaye Fournisseur modèle (refactoriser en fait une application existante).

L'affichage de la liste des événements est donc conditionnel en fonction d'un état géré dans le fournisseur.

Le problème est que lorsque j'appelle trop rapidement à notifyListeners(), j'obtiens cette exception:

════════ Exception interceptée par la bibliothèque de la fondation ════════

L'affirmation suivante a été émise lors de la distribution des notifications pour EventProvider:

setState () ou markNeedsBuild () appelé lors de la génération.

...

La notification d'envoi d'EventProvider était: Instance de "EventProvider"

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

Attendre quelques millisecondes avant d'appeler notifyListeners() résout le problème (voir la ligne commentée dans la classe de fournisseur ci-dessous).

Ceci est un exemple simple basé sur mon code (espérons pas trop simplifié):

fonction principale:

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

widget racine:

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();
            },
        );

    }
}

page d'accueil:

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(...)
        )
    }
}

fournisseur d'événements:

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();
    }
}

Quelqu'un peut-il expliquer ce qui se passe?

2
Yann39

Vous appelez fetchEvents depuis votre code de construction pour le widget racine. Dans fetchEvents, vous appelez notifyListeners, qui, entre autres, appelle setState sur les widgets qui écoutent le fournisseur d'événements. Il s'agit d'un problème car vous ne pouvez pas appeler setState sur un widget lorsque le widget est en cours de reconstruction.

Maintenant, à ce stade, vous pensez peut-être "mais la méthode fetchEvents est marquée comme async donc elle devrait être exécutée asynchrone pour plus tard". Et la réponse est "oui et non". La façon dont async fonctionne dans Dart est que lorsque vous appelez une méthode async, Dart tente d'exécuter autant de code de la méthode que possible de manière synchrone. En bref, cela signifie que tout code de votre méthode async qui précède un await va s'exécuter en tant que code synchrone normal. Si nous examinons votre méthode fetchEvents:

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();
}

Nous pouvons voir que le premier await se produit lors de l'appel à EventService().getEventsByUser(email). Il y a un notifyListeners avant cela, donc ça va être appelé de façon synchrone. Ce qui signifie appeler cette méthode à partir de la méthode de construction d'un widget sera comme si vous appeliez notifyListeners dans la méthode de construction elle-même, ce qui, comme je l'ai dit, est interdit.

La raison pour laquelle cela fonctionne lorsque vous ajoutez l'appel à Future.delayed Est que maintenant il y a un await en haut de la méthode, provoquant l'exécution asynchrone de tout ce qui se trouve en dessous. Une fois que l'exécution arrive à la partie du code qui appelle notifyListeners, Flutter n'est plus dans un état de reconstruction des widgets, il est donc sûr d'appeler cette méthode à ce stade.

Vous pouvez à la place appeler fetchEvents à partir de la méthode initState, mais cela se heurte à un autre problème similaire: vous ne pouvez pas non plus appeler setState avant l'initialisation du widget.

La solution est donc la suivante. Au lieu de notifier tous les widgets écoutant le fournisseur d'événements qu'il se charge, faites-le charger par défaut lors de sa création. (C'est très bien car la première chose qu'il fait après avoir été créé est de charger tous les événements, donc il ne devrait jamais y avoir de scénario où il ne doit pas être chargé lors de sa première création.) Cela élimine la nécessité de marquer le fournisseur comme chargement au début de la méthode, ce qui élimine à son tour la nécessité d'appeler notifyListeners là:

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();
}
6
Abion47

Le problème est que vous appelez notifyListeners deux fois dans une fonction. Je comprends, vous voulez changer l'état. Cependant, il devrait pas être la responsabilité de l'EventProvider de notifier l'application lors du chargement. Tout ce que vous avez à faire est que s'il n'est pas chargé, supposez qu'il se charge et mettez simplement un CircularProgressIndicator. N'appelez pas notifyListeners deux fois dans la même fonction, cela ne vous sert à rien.

Si vous voulez vraiment le faire, essayez ceci:

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