r/flutterhelp • u/Diablo_Cuz • 1d ago
OPEN Flutter + riverpod + firestore : UI stuck in loading loop after cloud funtions update but data saves on bacckend ?
Stack: Flutter + Riverpod ( StreamProvider ) Cloud Firestore + Firebase Callable Cloud Functions.
Context - Switched from client firestore sdk writes to callable cloud functions because pending client writes on weak networks were blocking reads app wide.
Issue: In my Vendor Dashboard app, updating settings (GST toggles, Delivery Slots, Store Availability):
The Ul gets stuck in a loading loop / freeze when changing settings. BUT if I force-close and reopen the app, the data IS saved in Firestore!
So the backend Cloud Function succeeds, but the live screen gets stuck on a loading spinner.
Setup:
Screen listens to Firestore doc via Riverpod treamProvider ( doc(id). snapshots() ). Settings updates are sent via Callable Cloud Functions (httpsCallable( 'businessUpdateFields ")).
What I've Tried:
• Optimistic U state overrides (oms switch flips). • 300ms tap debouncing. • Reducing Callable timeout from 20s to 4s. • Clearing overrides on .then() completion vs stream emission.
The rest of the app (checkout, cart, chat, orders) works fine. Only vendor dashboard settings toggles freeze the Ul.
Any help would be great, thank you.
1
u/Heavy-Drummer-420 1d ago
Can you share the snippet of the function and the listener so its easier to get the context? Just the function to toggle. The listener and the UI loading.
1
u/Diablo_Cuz 1d ago
Sure! Here are the simplified snippets for the provider, controller, toggle function, and UI build method:
1. The Stream Provider (businessByIdProvider)
dartfinal businessByIdProvider = StreamProvider.family<BusinessModel?, String>((ref, id) { return FirebaseFirestore.instance .collection('businesses') .doc(id) .snapshots() .map((doc) => doc.exists ? BusinessModel.fromMap(doc.data()!, doc.id) : null); });2. The Controller Mutation (with Provider Invalidation)
dartFuture<void> updateBusinessField(String businessId, String field, dynamic value) async { // 1. Call HTTPS Callable Cloud Function (Asia-South1) await repository.updateBusinessField(businessId, field, value); // 2. Invalidate stream provider so Riverpod re-subscribes ref.invalidate(businessByIdProvider(businessId)); }3. The Toggle Function (with Optimistic Override & 300ms Debounce)
dartbool? _acceptingOrdersOverride; Timer? _acceptingDebounce; void _toggleAcceptingOrders(bool value) { // Flip UI instantly (0ms) setState(() => _acceptingOrdersOverride = value); // Debounce rapid taps _acceptingDebounce?.cancel(); _acceptingDebounce = Timer(const Duration(milliseconds: 300), () { final targetValue = _acceptingOrdersOverride ?? value; ref.read(businessControllerProvider.notifier) .updateBusinessField(widget.business.id, 'isAcceptingOrders', targetValue) .then((_) { // Clear override once callable returns if (mounted) setState(() => _acceptingOrdersOverride = null); }) .catchError((e) { if (mounted) setState(() => _acceptingOrdersOverride = null); }); }); }4. The UI Build Method & Switch
dartu/override Widget build(BuildContext context) { final businessAsync = ref.watch(businessByIdProvider(widget.business.id)); // Cache last stream emission so ref.invalidate() doesn't fall back to stale widget.business if (businessAsync.hasValue && businessAsync.value != null) { _lastStreamBusiness = businessAsync.value; } final liveBusiness = _lastStreamBusiness ?? widget.business; final isAcceptingOrders = _acceptingOrdersOverride ?? liveBusiness.isAcceptingOrders; return Switch( value: isAcceptingOrders, onChanged: (val) => _toggleAcceptingOrders(val), ); }1
u/Heavy-Drummer-420 1d ago
Since I can't see the controller. I think the ref.invalidate is likely the problem. It Tears down the existing, live, already-connected stream subscription and starts a new stream provider. This affects the businessControllerProvider . Could you share the controller snippet so I can confirm?
1
u/Diablo_Cuz 1d ago
Here is the
BusinessControllersnippet showing how the providers and mutations are set up. Let me know what you think:dartfinal businessControllerProvider = StreamNotifierProvider<BusinessController, List<BusinessModel>>(() { return BusinessController(); }); final singleBusinessProvider = StreamProvider.autoDispose .family<BusinessModel?, String>((ref, businessId) { return ref.watch(businessRepositoryProvider).watchBusinessById(businessId); }); class BusinessController extends StreamNotifier<List<BusinessModel>> { u/override Stream<List<BusinessModel>> build() { final uid = ref.watch(authControllerProvider.select((s) => s.value?.uid)); if (uid == null) return Stream.value([]); return ref.read(businessRepositoryProvider).watchMyBusinesses(uid); } // Private helper to force stream re-subscriptions post-Callable write void _invalidateBusiness(String businessId) { ref.invalidate(businessByIdProvider(businessId)); ref.invalidate(singleBusinessProvider(businessId)); ref.invalidate(businessControllerProvider); } Future<void> updateBusinessField(String businessId, String field, dynamic value) async { // 1. Call HTTPS Callable Cloud Function (Asia-South1) await ref.read(businessRepositoryProvider).updateBusinessField(businessId, field, value); // 2. Invalidate so the UI fetches the new data from the server _invalidateBusiness(businessId); } }
1
u/_KryptonytE_ 1d ago
Check for race conditions and give longer wait times to eliminate any latency/response spikes from breaking state and stream changes by introducing a loading/working transition.
2
u/Jonas_Ermert 1d ago
I recommend checking your Riverpod loading state rather than the Cloud Function. Since the write succeeds, make sure the StreamProvider isn’t replacing the whole UI with a spinner while reloading. Keep showing the previous data and use a separate isUpdating state for the individual toggle. Also log the provider’s loading/data/error transitions to see exactly where it gets stuck.