firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.71k stars 3.97k forks source link

.onChildAdded.listen() breaks Flutter web #8392

Closed h-unterp closed 2 years ago

h-unterp commented 2 years ago

Summary

In Flutter web, It seems that when I call .onChildAdded.listen() on my firebase realtime database connection, that it just stops working. If I uncomment .onChildAdded.listen() in thestate.dart , database calls work as they should. This is evidenced by the calls I make to directdb inside of DogApp.widget.

However, when I run this exact same code in ios, there is no hang. FYI, i've done a simpler test (At bottom) and it does not break in the same way that the main example does, so somewhere in Flutter UI's relationship with the DB there is a bug.

Github Code

https://github.com/hunterpp/testdb

Good Output

This is print output when .onChildAdded.listen() is commented out on web, or run in iOS uncommented. As you can see, COMPLETE dog 2 is shown, which means that the directdb call in DogApp.widget is getting data back from the database

test doglist
in try doglist
finally doglist
in try getItems
finally getItems
returned
COMPLETE doglist
returned getItems
COMPLETE getItems
test dog 1
in try dog 1
finally dog 1
test dog 2
in try dog 2
finally dog 2
returned
COMPLETE dog 1
returned
COMPLETE dog 2

Bad Output

When .onChildAdded.listen() is called in web the dog 1 and dog 2 do not execute COMPLETE. Here

test doglist
in try doglist
finally doglist
in try getItems
finally getItems
returned
COMPLETE doglist
returned getItems
COMPLETE getItems
test dog 1
in try dog 1
finally dog 1
test dog 2
in try dog 2
finally dog 2

main.dart

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(ChangeNotifierProvider(
      create: (context) => TheState(),
      child: MaterialApp(
          home: const MyHomePage(
        title: 'ok',
      ))));
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      var state = Provider.of<TheState>(context, listen: false);
      state.dbInit();
    });
  }

  @override
  Widget build(BuildContext context) {
    // getting the size of the window
    var size = MediaQuery.of(context).size;
    var height = size.height;
    var width = size.width;
    return Scaffold(
        body: SafeArea(
            child: const SizedBox(height: 500, width: 500, child: DogList())));
  }
}

class DogList extends StatelessWidget {
  const DogList({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    directDB("doglist");
    return Consumer<TheState>(
      builder: (context, state, child) {
        var nums = state.getItems();
        if (nums == null) {
          return const Text("loading", textDirection: TextDirection.ltr);
        }
        var prod = List<int>.generate(10, (i) => i + 1);
        return CustomScrollView(
          slivers: <Widget>[
            SliverList(
              delegate: SliverChildListDelegate(
                prod
                    .map(
                      (model) => Container(
                        color: Colors.white,
                        child: DogApp(model),
                      ),
                    )
                    .toList(),
              ),
            )
          ],
        );
      },
    );
  }
}

class DogApp extends StatelessWidget {
  DogApp(this.num);
  final int num;

  @override
  Widget build(BuildContext context) {
    directDB("dog " + num.toString());
    var x = "ok " + num.toString();
    return SizedBox(
        height: 500,
        width: 500,
        child: Text(x, textDirection: TextDirection.ltr));
  }
}

thestate.dart

final DatabaseReference kDb = FirebaseDatabase.instance.ref();

class TheState extends ChangeNotifier {
  int? items;
  Query? _feedQuery;
  Future<bool> dbInit() {
    _feedQuery = kDb.child("tweet");
    _feedQuery!.onChildAdded.listen(_childAdded);
    initItems();
    return Future.value(true);
  }

  int? getItems() {
    return items;
  }

  void initItems() {
    var calledFrom = "getItems";
    try {
      print("in try" + " " + calledFrom);
      kDb.child('tweet').once().then((DatabaseEvent event) {
        print("returned" + " " + calledFrom);
        final snapshot = event.snapshot;
        if (snapshot.value != null) {
          var map = snapshot.value as Map<dynamic, dynamic>;
          items = 5;
          notifyListeners();
        }
      }).catchError((e, stackTrace) {
        print("in catchError" + " " + calledFrom);
      }).whenComplete(() => print("COMPLETE" + " " + calledFrom));
    } catch (error) {
      print("catch" + " " + calledFrom);
    } finally {
      print("finally" + " " + calledFrom);
    }
  }

  void _childAdded(DatabaseEvent event) {}
}

directdb.dart

void directDB(String calledFrom) {
  print("test" + " " + calledFrom);

  //FirebaseDatabase fd = FirebaseDatabase.instance;
  //fd.setLoggingEnabled(true);
  //DatabaseReference dr = fd.ref();
  try {
    print("in try" + " " + calledFrom);
    kDb.child('tweet').child("-MxvCNmiJ9lRNX8XkwL4").once().then((DatabaseEvent event) {
      print("returned");
      final snapshot = event.snapshot;
      if (snapshot.value != null) {
        var map = snapshot.value as Map<dynamic, dynamic>;
      }
    }).catchError((e, stackTrace) {
      print("in catchError" + " " + calledFrom);
    }).whenComplete(() => print("COMPLETE" + " " + calledFrom));
  } catch (error) {
    print("catch" + " " + calledFrom);
  } finally {
    print("finally" + " " + calledFrom);
  }
}

Simple Test (Works)

void main() async {
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  final DatabaseReference kDb = FirebaseDatabase.instance.ref();

  _feedQuery = kDb.child("tweet");
  _feedSubscription = _feedQuery!.onChildAdded.listen(
    (DatabaseEvent event) {
      print('Child added:');
    },
    onError: (Object o) {
      final error = o as FirebaseException;
      print('Error: ${error.code} ${error.message}');
    },
  );
  directDB("main");
  directDB("main");
}
darshankawar commented 2 years ago

@hunterpp Thanks for the detailed report. From the code you shared above, it seems you are using provider which is third party. Does the same behavior persist without using provider package ? If so, can you provide updated code sample without using it ?

h-unterp commented 2 years ago

Hi! What exactly do you mean by "3rd party provider"?

If by the invertase binary in the podfile, Ok.

But I fail to see how that binary would affect flutter web which is where the bug exists.

darshankawar commented 2 years ago

Since you are using var state = Provider.of<TheState>(context, listen: false);, I meant to ask, if the same behavior is replicable using the provider package, ie, using flutter's core code.

h-unterp commented 2 years ago

Sorry, I am new to Flutter. So I don't understand what you mean

darshankawar commented 2 years ago

@hunterpp Can you try the plugin's official example and see if you still get same behavior as you reported ?

https://github.com/firebase/flutterfire/tree/master/packages/firebase_database/firebase_database/example

h-unterp commented 2 years ago

Yes thank you, confirmed the official example works. Wondering what the difference between my code and the example is. My code is quite the minimal reproduction of the issue, is it using the API incorrectly? Or is this actually a bug? Thank you.

darshankawar commented 2 years ago

@hunterpp Looks like an implementation issue since the official example works as I verified as well. Please re-visit your implementation and compare it with the example and see where you could be missing a piece.

Closing for now, as using plugin example, the onChildAdded.listen() works as expected. If you disagree, write in comments and I'll reopen it.