1

I'm trying to create a Stream, which will be called in the main page. This Stream returns me a list from my database. I will be using this list to create several cards in the main screen, and whenever there is a new card or a card removed, I will refresh the screen.

This is my Stream:

Stream<List> readData() async*{
    Map<dynamic, dynamic> button_list = Map();
    List lst = [];

    final FirebaseUser user = await _auth.currentUser();

    final lstValues = databaseReference.child(user.uid+"/buttons/").onValue.forEach((element) {
      button_list = element.snapshot.value as Map;
      lst = button_list.values.toList();
      print(lst);
    });

    final lstStream = Stream.fromFuture(lstValues);

    await for(var event in lstStream) {
      yield lst;
    }
      
  }

This is the result from print(lst):

flutter: [{icon: delte, nome: Junior}, {icon: add, nome: Televisao}, {icon: bulb, nome: BAtata}]

This is the database: Databse

This is the main screen with the main code:

body: StreamBuilder(
        stream: _auth.readData(),
        initialData: 0,
        builder: (context, snapshot) {
          if (snapshot.hasError || snapshot.hasError){
            return Container(color: Colors.red);
          }
          if (!snapshot.hasData || !snapshot.hasData){
            return Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasData || snapshot.hasData){
            return GridView.count(

The problem is that the values are not being received in the Stream. In the main page. Whenever I try to use snapshot.data I get nothing. At the moment the only think is loading is the progress circular indicator, I'm not receiving the content from the Stream I have created.

16
  • You mentioned an error in the question title but there's no code in to it's reference. Share code for your StreamBuilder/StreamProvider! Commented Jan 23, 2021 at 5:28
  • @ASAD HAMMED i have updates the answer with the code and my database photo. Commented Jan 24, 2021 at 16:18
  • I'm trying to reproduce the error and InShaALLAH I'll be able to solve it :) Commented Jan 25, 2021 at 15:29
  • Ok, thank you, I'm trying to solve it too. If I find something will bring you feedback! Commented Jan 25, 2021 at 15:30
  • @ASADHAMEED I have updated the question with some code Commented Jan 25, 2021 at 15:35

5 Answers 5

1

Personally, I rather work with streams and rxdart than methods such as yield.

Within my firebase projects I use a construction like this:

// Get a database reference for the user
Future<DatabaseReference> _getUserRef() async {
    final FirebaseUser user = await _auth.currentUser();
    return FirebaseDatabase.instance
        .reference()
        .child('users')
        .child(user.uid);
}

// Get a reference to a specific user node. In you cause buttons
Future<DatabaseReference> _getButtonsRef() async {
    return (await _getUserRef()).child('buttons');
}

// Get the data as stream
Stream<List<MyButton>> getButtons() { // Not sure what data type you need
    return _getButtonsRef().asStream()
        .switchMap((ref) => ref.onValue) // Use on value to get new data if any changes
        .map((event) => event.snapshot.value != null ? // Map the value to the object you want or return an empty list
            MySnapshotMapper.buttonListFromSnapshot(event.snapshot.value) : List<MyButton>()
        );
}

In case you wonder about the MySnapshotMapper:

class MySnapshotMapper {
  static List<MyButton> buttonListFromSnapshot(Map snapshot) {
    return List<MyButton>.from(snapshot.values.map((snap) => MyButton.fromSnapshot(snap)));
  }
}

And of course the button:

class MyButton {
  // Not sure which fields it should have
  String name = '';
  double width = 10.0, height = 10;

  MyButton.fromSnapshot(Map snap) {
    name = snap['name'] ?? ''; // Use the value in the Map or or use a default value if not found
    width = snap['width']?.toDouble() || width;
    height = snap['height ']?.toDouble() || height ;
  }
}
Sign up to request clarification or add additional context in comments.

4 Comments

In the main page I will only need to call getButtons() then?
Yes but you would have to use the streambuilder. api.flutter.dev/flutter/widgets/StreamBuilder-class.html
@NiltonSchumacherF any luck so far?
I have given up on using realtime firebase, I have been working with firestore now and had much more success. I wasn't working with firestore because ESP32 had dificulties connecting to the firestore, but theres a new library out which is working great.
0

Step 1:

class EmployeeRepository {
  final CollectionReference collection =
  FirebaseFirestore.instance.collection('employees');

  Stream<QuerySnapshot> getStream() {
    /// Based on Firebase.auth you can collect user data here and pass as 
    /// Stream<QuerySnapshot> like below.
    return collection.snapshots();
  }
  
  Future<List<Employee>> buildData(AsyncSnapshot snapshot) async {
    List<Employee> list = [];

    /// Based on the user snapShot, you can convert into the List and return to
    /// the futurebuilder

    await Future.forEach(snapshot.data.docs, (element) async {
      list.add(Employee.fromSnapshot(element));
    });

    return Future<List<Employee>>.value(list);
  }
}

Step 2:

  EmployeeRepository employeeRepository = EmployeeRepository();

  @override
  Widget build(BuildContext context) {
    Widget loadProgressIndicator() {
      return Container(
        child: Center(child: CircularProgressIndicator()),
      );
    }

    return SafeArea(
        child: Scaffold(
            appBar: AppBar(
              title: Text('ListView'),
            ),
            body: StreamBuilder<QuerySnapshot>(
                stream: employeeRepository.getStream(),
                builder: (context, snapShot) {
                  if (snapShot.hasError ||
                      snapShot.data == null ||
                      snapShot.data.docs.length == 0 ||
                      snapShot.connectionState == ConnectionState.waiting) {
                    return loadProgressIndicator();
                  } else {
                    return FutureBuilder(
                        future: employeeRepository.buildData(snapShot),
                        builder: (context, futureSnapShot) {
                          if (futureSnapShot.hasError ||
                              futureSnapShot.connectionState ==
                                  ConnectionState.waiting ||
                              futureSnapShot.data.length == 0) {
                            return loadProgressIndicator();
                          } else {
                            return ListView.builder(
                              itemBuilder: (context, index) {
                                final employee = futureSnapShot.data[index];
                                return ListTile(
                                  title: Text(employee.employeeName),
                                );
                              },
                            );
                          }
                        });
                  }
                })));
  }

Building User Data

Comments

0

This what I think has happened and which is why the code is not working as expected:

  1. onValue function of the DocumentReference provides a Stream<Event> according to the latest documentation.

Stream<Event> onValue

  1. But since the forEach returns a Future it is counted and used as a Future & then converted to a Stream by using Stream.fromFuture()

Future forEach(void action(T element))

  1. While as forEach Returns a future, when completed it returns null as final value to the future.
Future forEach(void action(T element)) {
  _Future future = new _Future();
  StreamSubscription<T> subscription =
      this.listen(null, onError: future._completeError, onDone: () {
    future._complete(null);
  }, cancelOnError: true);
  subscription.onData((T element) {
    _runUserCode<void>(() => action(element), (_) {},
        _cancelAndErrorClosure(subscription, future));
  });
  return future;
}
  1. Finally the lst being returned instead of the event in the final for loop.
  await for (var event in lstStream) {
    yield lst;
  }

You can improve this code to make it work as following.

Stream<List> readData(user) async*{
    final lstValues = databaseReference.child(user.uid+"/buttons/").onValue.map((element) {
      Map button_list = element.snapshot.value as Map;
      List lst = button_list.values.toList();
      print(lst);
      return lst;
    }).toList();

    final lstStream = Stream.fromFuture(lstValues);

    await for(var event in lstStream) {
      yield event;
    }
      
  }

Check that I have made following changes:

  1. replaced forEach with map
  2. [Optional change] taken Firebase user as method dependency as it is not required to be fetched on every iteration
  3. [Optional change] moved lst & button_list inside the map execution block

I have not tested this code due to Firebase database dependency, but I have tested the theory on which this solution is based off of.

Here is the sample which I have tested:

Stream<List> readData() async* {

  final list = Stream.fromIterable([
    ['a'],
    ['a', 'b'],
    ['a', 'b', 'c'],
    ['a', 'b', 'c', 'd']
  ]).map((element) {
    print(element);
    return element;
  }).toList();

  final listStream = Stream.fromFuture(list);
  await for (var event in listStream) {
    yield event;
  }
}

I have replaced the Firebase document with a list of strings to make provide as much as resemblance as possible.

So in theory,

Stream.fromIterable([
    ['a'],
    ['a', 'b'],
    ['a', 'b', 'c'],
    ['a', 'b', 'c', 'd']
  ]) // Stream<List<String>> which can be similar to a list of documents

can replace

databaseReference.child(user.uid+"/buttons/").onValue // Stream<Event> which has a list of documents

2 Comments

I get the following error from your stream: I/flutter ( 1463): null W/System ( 1463): Ignoring header X-Firebase-Locale because its value was null. D/FirebaseAuth( 1463): Notifying id token listeners about user ( QzRG6ZV06tPMDprqHPs8h8d0AaB2 ). W/System ( 1463): Ignoring header X-Firebase-Locale because its value was null. D/FirebaseAuth( 1463): Notifying id token listeners about user ( QzRG6ZV06tPMDprqHPs8h8d0AaB2 ). I/flutter ( 1463): [{icon: bulb, nome: BAtata}, 3, {icon: delte, nome: Junior}, {icon: add, nome: Televisao}]
The problem is that it enters in the ìf(!snapshot.hasData)`condition, and keeps the circular progress bar
-1

Since FirebaseDatabase does not provide a stream of results you should use, Cloud FireStore

Here is the implementation of your code using cloud_firestore: ^0.16.0.

You will need to use subCollections for replicated the exact structure as RealTime Database.

1.Create a datamodel for the data you want to store and retrieve from firestore to made things easier.

class ButtonData{
  final String name, icon;

  ButtonData({this.name, this.icon});
}
  1. Create a Stream that returns a list of documents from cloud firestore subCollection.

    Stream<List<ButtonData>> getData(){
    
    return users
        .doc(FirebaseAuth.instance.currentUser.uid)
        .collection('buttons').snapshots().map(buttonsFromQuerySnapshot);
     }
    
  2. Create a function that converts QuerySnapshot from firestore to a list of required objects. buttonsFromQuerySnapshot

    List<ButtonData> buttonsFromQuerySnapshot(QuerySnapshot querySnapshot){
    
    return querySnapshot.docs.map((DocumentSnapshot snapshot) {
    
      return ButtonData(name: snapshot.data()['name'].toString(), icon: snapshot.data()['icon'].toString());
    }).toList();
     }
    
  3. Use a streamBuilder to show results from the stream.

     StreamBuilder<List<ButtonData>>(
           stream: getData(),
             builder: (context, snapshot){
    
             if (snapshot.hasData){
               final List<ButtonData> buttons = snapshot.data;
               return ListView.builder(itemBuilder: (context, index){
                 return Column(
                   children: [
                     Text(buttons[index].name),
                     Text(buttons[index].icon),
                   ],
                 );
               });
             }
    
             return const Center(child: CircularProgressIndicator(),);
    
             }),
    

I would recommend you to store icons as integer values. Here you can find a list of Material Icons and their integer values.

You can then display icons using their retrieved integer values. See this answer https://stackoverflow.com/a/59854460/10285344 (Haven't tried this)

10 Comments

Whenever there is a change in the Database, will I receive the new data?
Could you just modify a bit of the answer. Where should I add step 3 in the mainpage? or in a separete file with only the class?
onValue.listner is listening to changes and it will update buttons using setState whenever there's a change in database.
@ASADHAMMED Getting this error: VERBOSE-2:ui_dart_state.cc(177)] Unhandled Exception: NoSuchMethodError: The method 'forEach' was called on null. Receiver: null Tried calling: forEach(Closure: (dynamic, dynamic) => Null) #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:51:5) #1 new LinkedHashMap.from (dart:collection/linked_hash_map.dart:88:11) #2 _HomeState.initState.<anonymous closure>.<anonymous closure> package:automacaoaplicativo/screens_codes/home_page.dart:25 #3 State.setState
If it was for could firestore, would it be easier?
|
-1

I solved a very similar problem about loading the functions a user can execute according to their profile to build the interface. It's basically handling an async and futures issue. For me, Provider made the deal. I will try to put everything in order and paste my code for reference, note I did not have to make changes in the state, I just needed the initial information:

  1. Create a multiprovider for your app
  2. Define the Provider to call your API to get the initial information of the cards.
  3. Pass this information as a parameter to your widget using Provider.of
  4. Use this provider info in InitState()
  5. Options for managing changes... Copy the provider info into an object you can handle or define API calls to your provider to update changes dynamically (I did not went through this)

Check relevant parts of code you may be interested in: Provider class and API call:

class UserFunctionProvider {
  Future<List<UserFunction>> loadUserFunctions() async {
    return await APICall.profileFunctions();
  }
}

  static Future<List<UserFunction>> profileFunctions() async{
    List<UserFunction> functionList = [];
    UserFunction oneFunction;
    final cfg = new GlobalConfiguration();
    final token = window.localStorage["csrf"];
    var res = await http.get('${cfg.get('server')}:${cfg.get('port')}/get_user_functions',
        headers: {
          'Content-type': 'application/json',
          'Accept': 'application/json',
          'Authorization': 'Bearer $token'
        }
    );
    int i = 0;
    jsonDecode(res.body).forEach((element) {
      oneFunction = new UserFunction.fromJson(element);
      oneFunction.tabControllerIndex = i;
      i++;
      functionList.add(oneFunction);
    });
    return functionList;
  }

Defining a Multiprovider and passing it to the relevant widget (it was home in my case)

void main() async {
  GlobalConfiguration().loadFromMap(AppConfiguration.appConfig);
  Logger.root.level = Level.ALL; // defaults to Level.INFO
  Logger.root.onRecord.listen((record) {
    print(
        '${record.level.name}: ${record.time}: ${record.loggerName}: ${record.message}');
  });
  WidgetsFlutterBinding.ensureInitialized();
  FlutterError.onError = (FlutterErrorDetails details) {
    FlutterError.dumpErrorToConsole(details);
    if (kReleaseMode)
      exit(1);
  };
  runApp(
    MultiProvider(
      providers: [
        FutureProvider(create: (_) => UserFunctionProvider().loadUserFunctions()),
      ],
      child: MyApp()
    )
  );
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.

  @override
  Widget build(BuildContext context) {
    String myLocale;
    try{
      myLocale = Platform.localeName;
    }catch(e){
      myLocale = 'es_ES';
      print('Language set to Spanish by default.\n Error retrieving platform language: $e');
    }
    initializeDateFormatting(myLocale, null);
    return MaterialApp(
      title: 'Sanofi admin',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: VerifySession().loadScreen(HomeScreen(Provider.of<List<UserFunction>>(context)))
      );
  }
}

Receiving the parameter from the provider into the Widget (as listOfUserFunction):

class HomeScreen extends StatefulWidget {
  HomeScreen(this.listOfUserFunction);

  final List<UserFunction> listOfUserFunction;

  @override
  HomeScreenState createState() => HomeScreenState();
}

class HomeScreenState extends State<HomeScreen>
    with SingleTickerProviderStateMixin {
  final log = Logger('HomeScreenState');
  TabController tabController;
  int active = 0;
  UserFunction oneFunction;
  @override
  void initState() {
    super.initState();
    tabController = new TabController(vsync: this, length: widget.listOfUserFunction.length, initialIndex: 0)
      ..addListener(() {
      setState(() {
        active = tabController.index;
      });
      });
  }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.