Canceling a Firebase Listener inside a ChangeNotifier

Viewed 309

When I try to cancel a Firestore listener with ProductsService().cancel(). I get the error:

[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: LateInitializationError: Field 'productsSubscription' has not been initialized.

Without late also doesn't work:

StreamSubscription<QuerySnapshot>? productsSubscription;

My code:

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

class ProductsService extends ChangeNotifier {
  late StreamSubscription<QuerySnapshot> productsSubscription;

  void cancel() => productsSubscription.cancel();

  void getMenuProducts() async {
    CollectionReference productsReference = FirebaseFirestore.instance.collection('products');
    productsSubscription = productsReference.snapshots().listen((snapshot) => print(snapshot.docs.length));
  }
}

I know calling .cancel() on it works inside a context. But I am using this in a ChangeNotifier with Riverpod.

The stream with Riverpod works fine. The real issue is that when the user logs out and a new user logs in, the .listen still seems to be stuck on the old firebase user! and doesn't allow queries. Firebase rules reject them with [cloud_firestore/permission-denied] - and when I restart the app the query works.

What I noticed is that If I can get the .listen canceled at the time of logging out, the query works when the new user logs in.

So if anyone can help me initialise this properly, or get Firebase to see the new user, it would help very much. Or any other suggestions.

2 Answers

If you're using Riverpod then you could use a ChangeNotifierProvider.autoDispose and call cancel when disposing (or override your class dispose to call cancel) or listen to the username inside that provider and cancel when it fires an update (login / logout status)

This seems to work, it just feels fragile, but ok so far.

The service class looks like this, with a cancel method in it.

class ProductsServiceNotifier extends ChangeNotifier {
  final FirebaseFirestore _db = FirebaseFirestore.instance;
  StreamSubscription<QuerySnapshot<Object?>>? productsSubscription;
  late List<ProductsModel> allProductsList = [];
  bool isLoading = true;

  void getShopProducts() async {
    var productsRef = _db.collection('products');
    productsSubscription = productsRef
        .where('entity', isEqualTo: sharedPrefs.activeShopEntityCode)
        .where('active', isEqualTo: true)
        .snapshots()
        .listen((snapshot) async {
      allProductsList = snapshot.docs.map((doc) => ProductsModel.fromMap(doc)).toList();
      isLoading = false;
      notifyListeners();
    });
  }

  Future cancelSub() async {
    if (productsSubscription != null) await productsSubscription?.cancel();
  }
}

And then calling cancel on log out like so:

   TextButton(
      child: Text('Sign out'),
      onPressed: () async {
        await context.read(menuProductsNotifier).cancelSub();
        FirebaseAuth.instance.signOut();
        Navigator.of(context).pushNamedAndRemoveUntil('/', (Route<dynamic> route) => false);
      },              
    ),

What is important is to call the cancel() method every time before I start a new lister. Otherwise the existing listener seems to get stuck somewhere in memory and causes the Firestore rules permission denied auth issue. (The use case is that the user can call products for different entities which starts a new query, and need to listen to them.)

  • autodispose in my Riverpod provider does not work in my scenario.

  • I chose to not use autodispose as well as it lets me call cancel() on the listener, which off course I wont be able to do if it is disposed.

  • Calling cancel in dispose gives the error "Looking up a deactivated widget's ancestor is unsafe." So that doesnt work for me - I wish it did.

I am open for any better ways of doing this. Thanks for the suggestions!

Related