David L
David L

Reputation: 1364

Why do "awaits" fail in Firebase RTDB?

I am currently using Firebase RTDB version 8.2 and every time I update to version 9.1 I have serious problems with queries that use await in my app.

Basically the problem is that as of version 9 the awaits do not work in a predictable way.

I made the following code to check it...

The code has a query button that when pressed (_submit()) brings up a list of articles in the database and returns the result of the sum of the quantities:

 Future<void> _submit() async {
   List<ItemModel> itemsList = [];
   itemsList = await loadItems();
   int _sumQty = itemsList.fold(0, (a, b) => a + b.quantity!);
   print('$_sumQty was the sum of quantities');
 }

If this code is used with version 8.2.0 of Firebase on the web the result is as expected:

{item: iPhone, quantity: 5} {item: Samsung, quantity: 10} {item: LG, quantity: 15} {item: NTC, quantity: 20} {item: Zenith, quantity: 25} {item: Sony, quantity: 30} {item: JVC, quantity: 40}

7 Items were loaded

145 was the sum of quantities

And if the same code is used with the latest version of Firebase (9.1.4) on the Web the result is completely wrong:

{item: iPhone, quantity: 5}

1 Items were loaded

5 was the sum of quantities

{item: Samsung, quantity: 10} {item: LG, quantity: 15} {item: NTC, quantity: 20} {item: Zenith, quantity: 25} {item: Sony, quantity: 30} {item: JVC, quantity: 40}

Note that in the new version the execution does not wait for the complete list and only fetches the first of the components causing a mess if you are using awaits in some parts of your code.

Although the error occurs 100% of the time on the Web, this situation also occurs (eventually) on Mobile (Android)

The questions are:

1. What is the origin of changing the behavior of Await in recent versions of Firebase?

2. If the recommendation is to migrate to the most recent version, what changes should be made in the code to ensure that this unexpected behavior does not occur in the queries?

ATTACHMENT - FULL CODE TO FIREBASE 8.2.0:

import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_database/ui/firebase_list.dart';
import 'package:flutter/material.dart';
 
class Prueba extends StatefulWidget {
 const Prueba({Key? key}) : super(key: key);
 
 @override
 State<Prueba> createState() => _PruebaState();
}
 
class ItemModel {
   String? item;
   int? quantity;
 
   ItemModel({
       this.item,
       this.quantity,
   });
}
 
class _PruebaState extends State<Prueba> {
 
@override
 void dispose() {
   super.dispose();
 }
 
@override
void initState() {
 super.initState(); 
}
 
 @override
 Widget build(BuildContext context) {
 
   return Scaffold(
     appBar: AppBar(
       title: const Text('Async InitState Test'),
     ),
     body: createButton('Query'),
 
   );
 }
 
   Widget createButton(String texto) {
   return Container(
     width: double.infinity,
     padding: const EdgeInsets.only(top:30.0, left:10.0, right:10.0),
     child: ElevatedButton(
       onPressed: _submit,
       child: Text(texto, style: Theme.of(context).textTheme.headline6),
     ),
   );
 }
 
 Future<void> _submit() async {
   List<ItemModel> itemsList = [];
   itemsList = await loadItems();
   int _sumQty = itemsList.fold(0, (a, b) => a + b.quantity!);
   print('$_sumQty was the sum of quantities');
 }
 
 
 Future<List<ItemModel>> loadItems() async {
 
   final List<ItemModel> items = [];
 
   String path = '/items';
   Query resp = FirebaseDatabase.instance.reference().child(path);
 
   FirebaseList(
     query: resp,
     onChildAdded: (i, element) {
       print(element.value);
       ItemModel temp = ItemModel()
         ..item = element.value["item"]
         ..quantity = element.value["quantity"];
       items.add(temp);
     },
     onError: (e) => print(e.message)
   );
 
   await resp.once().then((snapshot) {
     print("${items.length} Items were loaded");
   });
 
   return items;
 }
}

To apply it on Firebase 9.1.4, your need to do some changes on loadItems:

  Future<List<ItemModel>> loadItems() async {

    final List<ItemModel> items = []; 

    String path = '/empresas/-ME9qZY5k8RxpCymZlV2/items';
    Query resp = FirebaseDatabase.instance.ref().child(path);

    FirebaseList(
      query: resp,
      onChildAdded: (i, element) {
        print(element.value);
        Map<dynamic, dynamic> map = element.value as dynamic; 
        ItemModel temp = ItemModel()
          ..item = map["item"]
          ..quantity = map["quantity"];
        items.add(temp);
      },
      onError: (e) => print(e.message)
    );

    await resp.once().then((snapshot) {
      print("${items.length} Items were loaded");
    });

    return items;
  }
}

Upvotes: 0

Views: 54

Answers (1)

Frank van Puffelen
Frank van Puffelen

Reputation: 600141

Don't combine await and then.

Instead of:

await resp.once().then((snapshot) {
  print("${items.length} Items were loaded");
});

Do:

var snapshot = await resp.once();
print("${items.length} Items were loaded");

There is no longer a need to use onChildAdded for this type of operation. In recent versions of the FlutterFire libraries for Firebase, you can listen to onValue, or use get and then loop over the children of the snapshot you get back.

With that simplification, your code could be:

Future<List<ItemModel>> loadItems() async { 
  Query query = FirebaseDatabase.instance.reference().child('/items');

  var event = await query.once();

  final List<ItemModel> items = event.snapshot.children.map((childSnapshot) {
    var data = Map<String,dynamic>.from(childSnapshot.value as dynamic);
    return ItemModel()
      ..item     = data["item"]
      ..quantity = data["quantity"];
  }).toList();

  return items;
}

Upvotes: 1

Related Questions