web-dev-qa-db-ja.com

構築中に呼び出されるFlutterプロバイダーのsetState()またはmarkNeedsBuild()

イベントのリストをロードし、データのフェッチ中にロードインジケータを表示したい。

私はしようとしていますプロバイダーパターン(実際に既存のアプリケーションをリファクタリングしています)。

したがって、イベントリストの表示は、プロバイダーで管理されているステータスに応じて条件付きです。

問題は、notifyListeners()の呼び出しが速すぎると、次の例外が発生することです。

foundation財団ライブラリが例外をキャッチしました════════

EventProviderの通知をディスパッチしているときに次のアサーションがスローされました。

ビルド中に呼び出されるsetState()またはmarkNeedsBuild()。

...

EventProvider送信通知は次のとおりです: 'EventProvider'のインスタンス

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

notifyListeners()を呼び出す前に数ミリ秒待機して問題を解決します(以下のプロバイダークラスのコメント行を参照)。

これは私のコードに基づいた単純な例です(過度に簡略化しないでください)。

主な機能

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

ルートウィジェット

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

    }
}

ホームページ

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

イベントプロバイダー

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

誰かが何が起こるか説明できますか?

2
Yann39

ルートウィジェットのビルドコード内からfetchEventsを呼び出しています。 fetchEvents内でnotifyListenersを呼び出します。これは、とりわけ、イベントプロバイダーをリッスンしているウィジェットでsetStateを呼び出します。ウィジェットが再構築中の場合、ウィジェットでsetStateを呼び出せないため、これは問題です。

この時点で、「fetchEventsメソッドはasyncとしてマークされているため、後で非同期に実行する必要がある」と考えるかもしれません。そして、その答えは「はい、いいえ」です。 Dartでasyncが機能する方法は、asyncメソッドを呼び出すと、Dartがメソッド内のコードを可能な限り同期的に実行しようとすることです。つまり、asyncが通常の同期コードとして実行される前にあるawaitメソッド内のすべてのコードを意味します。 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();
}
_

最初のawaitEventService().getEventsByUser(email)の呼び出し時に発生することがわかります。その前にnotifyListenersがあるため、同期的に呼び出されます。つまり、ウィジェットのビルドメソッドからこのメソッドを呼び出すのは、ビルドメソッド自体でnotifyListenersを呼び出した場合と同じです。これは、すでに述べたように禁止されています。

_Future.delayed_への呼び出しを追加すると機能する理由は、メソッドの上部にawaitがあり、その下にあるすべてのものが非同期で実行されるためです。実行がnotifyListenersを呼び出すコードの部分に到達すると、Flutterはウィジェットを再構築する状態ではなくなるため、その時点でそのメソッドを呼び出しても安全です。

代わりにfetchEventsメソッドからinitStateを呼び出すこともできますが、これは別の同様の問題にぶつかります。ウィジェットが初期化される前にsetStateを呼び出すこともできません。

それで解決策はこれです。読み込み中のイベントプロバイダーをリッスンしているすべてのウィジェットに通知する代わりに、作成時にデフォルトで読み込まれるようにします。 (作成後に最初に行うのはすべてのイベントの読み込みであるため、これで問題ありません。最初に作成したときに読み込まないようにする必要はありません。)これにより、プロバイダーをとしてマークする必要がなくなります。メソッドの最初にロードすることで、そこでnotifyListenersを呼び出す必要がなくなります。

_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

問題は、1つの関数でnotifyListenersを2回呼び出すことです。わかった、状態を変えたい。ただし、アプリの読み込み時にアプリに通知するのは、EventProviderの責任ですnotです。あなたがしなければならないのは、それがロードされていない場合、それがロードしていると仮定し、CircularProgressIndicatorを置くだけです。同じ関数でnotifyListenersを2回呼び出さないでください。効果がありません。

あなたが本当にそれをしたいなら、これを試してください:

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