Canceling a Firebase Listener inside a ChangeNotifier

167

Solution 1

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!

Solution 2

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)

Share:
167
Wesley Barnes
Author by

Wesley Barnes

Updated on December 06, 2022

Comments

  • Wesley Barnes
    Wesley Barnes over 1 year

    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.

  • Wesley Barnes
    Wesley Barnes over 2 years
    Hi EdwynZN, thank you. I used autodispose and get the same result unfortunately.
  • Wesley Barnes
    Wesley Barnes over 2 years
    What I noticed though is that even if I do get the listener disposed inside a context, and log the user out and back in, I still get [cloud_firestore/permission-denied] - unless I completely reload the app. This should not be necessary. There must be an issue with FirebaseAuth holding onto the user or something on that listener. Reloading the FB user does not work too. It can't be an issue with my Firestore rules, the rules work when the app is reloaded. I will post a separate question for this.
  • EdwynZN
    EdwynZN over 2 years
    you could update this question with your Riverpod code and how you control your auth and change notifier, maybe its something with Firebase or maybe a Future waiting where it shouldn't
  • Wesley Barnes
    Wesley Barnes over 2 years
    I created a new app to reproduce a bare minimum of the code and the listener stops as it should on log out and back in. There must be something wrong with how I navigate and remove contexts or something, super confused, will revert back.