Description
Is there an existing issue for this?
- I have searched the existing issues.
Which plugins are affected?
Firestore
Which platforms are affected?
Android, iOS
Description
The issue can be reproduced both on iOS and Android (simulator and physical device). I had to dig deep to be able to narrow it down to a simple function. The gist of the issue is that when the network is disabled and then enabled later, FieldValue.increment()
may never reach Cloud Firestore, or maybe it reaches it, but there is some conflict there. When your balance is 0 and you call increment(-10)
and then increment(10)
, you can end up with a balance of -10, which is wrong. You should end up with a balance of 0 always.
This issue can be reproduced with a real network on a real device in real-life conditions, but it's hard and very cumbersome. This is why in my reproduction case, I used firestore.disableNetwork()
and firestore.enableNetwork()
.
Also note that when you call the simulate
function once, sometimes it can work as expected. I would say it causes the bug about 40-50% of the time. This is why I created the fastSimulate
function that increases the chance of missing events. When we call simulate
or fastSimulate
, the expectation is that the value will always be 0 after all operations are synced with Firestore because we subtract and add the same amount.
Future<void> simulate() async {
increment(-1);
await firestore.disableNetwork();
increment(1);
await firestore.enableNetwork();
}
Reproducing the issue
- Install
firebase_core: 2.31.0
- Install
cloud_firestore: 4.17.3
- Configure Firebase for the Flutter project and have
FirebaseOptions.currentPlatform
ready. - Import
FirebaseOptions
to themain.dart
file. - Replace
colId
,docId
, andvalueId
with your values. - Create a field in your document, make it an integer, and assign the value of 0.
- Launch the app.
- Try to press
simulate
a couple of times with a delay of 2-3 seconds. - If regular
simulate
doesn't work, try to pressfastSimulate
. - After you have completed steps 8 and 9, you should have a value of
-1
or lower.
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
// todo: add your path
import 'your-location-of-firebase-options';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: FirebaseOptionsDevelopment.currentPlatform,
);
runApp(const App());
}
// todo: update values
const colId = 'your-collection-name';
const docId = 'your-document-id';
const valueId = 'your-field-name';
final firestore = FirebaseFirestore.instance;
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
int id = 0;
int value = 0;
StreamSubscription<dynamic>? sub;
@override
void initState() {
super.initState();
subscribe();
}
@override
void dispose() {
unawaited(sub?.cancel());
super.dispose();
}
void subscribe() {
sub = firestore.collection(colId).doc(docId).snapshots().listen(
(data) {
setState(() {
value = data[valueId] as int? ?? 0;
});
},
);
}
Future<void> simulate() async {
increment(-1);
await firestore.disableNetwork();
increment(1);
await firestore.enableNetwork();
}
void increment(int value) {
unawaited(asyncIncrement(value, id++));
}
Future<void> asyncIncrement(int value, int myId) async {
print('[START] $myId - increment($value) ${DateTime.now()}');
await firestore.collection(colId).doc(docId).update({
valueId: FieldValue.increment(value),
});
print('[END] $myId - increment($value) ${DateTime.now()}');
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.white,
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Row(),
Text(
value.toString(),
style: const TextStyle(
fontSize: 50,
),
),
TextButton(
onPressed: simulate,
child: const Text('Simulate (50% reproduction guarantee)'),
),
TextButton(
onPressed: () async {
final simulations = <Future<void>>[];
for (var i = 0; i < 50; i++) {
simulations.add(simulate());
}
await Future.wait(simulations);
},
child: const Text('Simulate (95% reproduction guarantee)'),
),
],
),
),
);
}
}
Firebase Core version
2.31.0
Flutter Version
3.22.2
Relevant Log Output
No response
Flutter dependencies
Expand Flutter dependencies
snippet
Replace this line with the contents of your `flutter pub deps -- --style=compact`.
Additional context and comments
Reproducible on the latest versions as of Jun 14th 2024.
firebase_core: 3.1.0
cloud_firestore: 5.0.1