I have a Flutter widget that listens to 2 Firestore queries:
class ExampleWidget extends StatefulWidget {
const ExampleWidget({super.key});
@override
State<ExampleWidget> createState() => _ExampleWidgetState();
}
class _ExampleWidgetState extends State<ExampleWidget> {
@override
void initState() {
FirebaseFirestore.instance
.collection('items')
.where('tags', arrayContains: 'foo')
.snapshots()
.listen(
(items) => print('Got items from query 1: ${items.docs.length}'));
FirebaseFirestore.instance
.collection('items')
.snapshots()
.listen(
(items) => print('Got items from query 2: ${items.docs.length}'));
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(body: Center(child: Text('Example')));
}
}
The widget is rendered directly in the main.dart
file:
Future<void> main() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: ExampleWidget(),
);
}
}
Here's are the documents:
items:
item#1:
tags: ['foo']
item#2:
tags: ['foo']
item#3:
tags: ['bar']
When I run the app in production, I see the following log:
Got items from query 1: 2
Got items from query 2: 3
When I run the app with Firebase Emulators, I see the following log:
Got items from query 1: 2
Got items from query 2: 2
Got items from query 2: 3
I'm expecting each query to receive exactly one event for 2 and 3 matched items respectfuly (like when running in prod). Why is the listener for the 2nd query executed twice?
The reason behind 2nd query receiving a duplicate response is Firestore local caching: the result of the 1st query is saved locally and is used as the first source of data for the 2nd query before the actual result arrives from the server. The reason the behaviour is different between emulator and production is latency, here is the sequence of events for each:
Emulator:
1. query 1 is executed
2. result from query 1 is received (2) immediately and saved into cache
3. query 2 is executed
4. query 2 uses the result from cache (2)
5. query 2 receives the correct result (3) from the server
Production:
1. query 1 is executed
3. query 2 is executed
4. result from query 1 is received (2) and saved into cache
5. result from query 2 is received (3) and saved into cache
More information can be found in these GH issues in the flutterfire and firebase-js-sdk repos: