From e3d31bf178ae041c83f250aa607e7963ba022b70 Mon Sep 17 00:00:00 2001 From: Mike Allen Date: Sun, 4 Oct 2026 16:11:50 -0700 Subject: [PATCH 1/2] fix(ui_firestore): don't setState after dispose when fetching a page _listenQuery delays its setState with a microtask so that fetchMore can be called from a child's build. That build can be in the frame that removes the FirestoreQueryBuilder (a list item scrolled away, a route torn down), so the microtask runs after dispose and throws. --- .../lib/src/query_builder.dart | 7 +++- .../test/query_builder_test.dart | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/packages/firebase_ui_firestore/lib/src/query_builder.dart b/packages/firebase_ui_firestore/lib/src/query_builder.dart index 8511453a..15b4eef8 100644 --- a/packages/firebase_ui_firestore/lib/src/query_builder.dart +++ b/packages/firebase_ui_firestore/lib/src/query_builder.dart @@ -165,8 +165,11 @@ class _FirestoreQueryBuilderState } // Delaying the setState so that fetchNextpage can be used within a child's - // "build" – most commonly ListView's itemBuilder - Future.microtask(() => setState(() {})); + // "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 { From 7ffe1f702705995795b6d7b31572f0aec105079c Mon Sep 17 00:00:00 2001 From: Michael Allen Date: Sun, 4 Oct 2026 16:32:40 -0700 Subject: [PATCH 2/2] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/firebase_ui_firestore/lib/src/query_builder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firebase_ui_firestore/lib/src/query_builder.dart b/packages/firebase_ui_firestore/lib/src/query_builder.dart index 15b4eef8..f574abc6 100644 --- a/packages/firebase_ui_firestore/lib/src/query_builder.dart +++ b/packages/firebase_ui_firestore/lib/src/query_builder.dart @@ -164,7 +164,7 @@ class _FirestoreQueryBuilderState _snapshot = _snapshot.copyWith(isFetching: true); } - // Delaying the setState so that fetchNextpage can be used within a child's + // 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(() {