イベントのリストをロードし、データのフェッチ中にロードインジケータを表示したい。
私はしようとしていますプロバイダーパターン(実際に既存のアプリケーションをリファクタリングしています)。
したがって、イベントリストの表示は、プロバイダーで管理されているステータスに応じて条件付きです。
問題は、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();
}
}
誰かが何が起こるか説明できますか?
ルートウィジェットのビルドコード内から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();
}
_
最初のawait
はEventService().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();
}
_
問題は、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();
}