diff --git a/packages/firebase_ui_firestore/lib/src/query_builder.dart b/packages/firebase_ui_firestore/lib/src/query_builder.dart index 8511453a..f574abc6 100644 --- a/packages/firebase_ui_firestore/lib/src/query_builder.dart +++ b/packages/firebase_ui_firestore/lib/src/query_builder.dart @@ -164,9 +164,12 @@ class _FirestoreQueryBuilderState _snapshot = _snapshot.copyWith(isFetching: true); } - // Delaying the setState so that fetchNextpage can be used within a child's - // "build" – most commonly ListView's itemBuilder - Future.microtask(() => setState(() {})); + // Delaying the setState so that fetchMore can be used within a child's + // "build" – most commonly ListView's itemBuilder. That build can be in the + // frame that removes this widget, so it may be gone by now. + Future.microtask(() { + if (mounted) setState(() {}); + }); final expectedDocsCount = (_pageCount + 1) * widget.pageSize diff --git a/packages/firebase_ui_firestore/test/query_builder_test.dart b/packages/firebase_ui_firestore/test/query_builder_test.dart index 8328bcfe..86b8692c 100644 --- a/packages/firebase_ui_firestore/test/query_builder_test.dart +++ b/packages/firebase_ui_firestore/test/query_builder_test.dart @@ -274,6 +274,44 @@ void main() { expect(snapshot.hasData, isTrue); expect(snapshot.isFetching, isFalse); }); + + testWidgets('scrolled away in the frame that its builder calls fetchMore', ( + tester, + ) async { + final controller = ScrollController(); + addTearDown(controller.dispose); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ListView( + controller: controller, + cacheExtent: 0, + itemExtent: 1000, + children: [ + FirestoreQueryBuilder( + query: query, + pageSize: 2, + builder: (context, s, _) { + if (s.hasMore) s.fetchMore(); + return const SizedBox(); + }, + ), + const SizedBox(), + const SizedBox(), + ], + ), + ), + ); + + // The snapshot rebuilds the query builder, whose builder fetches the + // next page, and the list drops it while laying out that same frame. The + // setState that fetchMore delays runs after it is disposed. + query.emit(3, size: 3, fromCache: false); + controller.jumpTo(2000); + await tester.pump(); + expect(tester.takeException(), isNull); + }); } class FakeQuery extends Fake implements Query {