2

I'm having an issue with StreamBuilder and ListView.

My initial build works as intended, loading all nodes from my DB and adding them to ListView:

--Node 1 --Node 2 --Node 3

However, when a new node is added to the DB (Node 4), the StreamBuilder recognizes the change and appends the entire list of nodes to the ListView, resulting a duplicate data:

--Node 1 --Node 2 --Node 3 --Node 1 --Node 2 --Node 3 --Node 4


class _HomeScreenState extends State<HomeScreen> {
  DatabaseReference usersChatsRef =
      FirebaseDatabase().reference().child('users-chats');

  @override
  void initState() {
    super.initState();

    Stream<List<UsersChats>> getData(User currentUser) async* {
      var usersChatsStream = usersChatsRef.child(currentUser.uid).onValue;
      var foundChats = List<UsersChats>();

      await for (var userChatSnapshot in usersChatsStream) {
        Map dictionary = userChatSnapshot.snapshot.value;
        if (dictionary != null) {
          for (var dictItem in dictionary.entries) {
            UsersChats thisChat;
            if (dictItem.key != null) {
              thisChat = UsersChats.fromMap(dictItem);
            } else {
              thisChat = UsersChats();
            }
            foundChats.add(thisChat);
          }
        }

        yield foundChats;
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final user = Provider.of<User>(context);

    return Scaffold(
      appBar: AppBar(),
      body: StreamBuilder<List<UsersChats>>(
          stream: getData(user),
          builder:
              (BuildContext context, AsyncSnapshot<List<UsersChats>> snap) {
            if (snap.hasError || !snap.hasData)
              return Text('Error: ${snap.error}');
            switch (snap.connectionState) {
              case ConnectionState.waiting:
                return Text("Loading...");
              default:
                return ListView.builder(
                  itemCount: snap.data.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(snap.data[index].id),
                      subtitle: Text(snap.data[index].lastUpdate),
                    );
                  },
                );
            }
          }),
    );
  }
}
class UsersChats {
  String id;
  String lastUpdate;

  UsersChats(
      {this.id,
      this.lastUpdate});

  factory UsersChats.fromMap(MapEntry<dynamic, dynamic> data) {
    return UsersChats(
        id: data.key ?? '',
        lastUpdate: data.value['lastUpdate'] ?? '');
  }
}

I'm referencing the stream outside of the build method because I need to perform multiple async functions on the stream (as discussed in this thread How do I join data from two Firestore collections in Flutter?).

Any help would be greatly appreciated!

Jack
  • 49
  • 5
  • It sounds like `usersChatsRef.child(currentUser.uid).onValue` emits the entire list on every change, instead of pushing only the difference. Which means you don't have to "add" to `foundChat` and can just erase the previous list. – Rémi Rousselet Jan 12 '20 at 17:14
  • Thanks for the quick reply! Do you know how I might modify the `getData` function as a result? Have been trying different approaches and haven't had any luck so far. – Jack Jan 12 '20 at 18:15
  • 1
    Try clearing `foundChats` in the first line of the "await for" – Rémi Rousselet Jan 12 '20 at 18:19
  • Amazing!! That did it, thank you so much!!! – Jack Jan 12 '20 at 18:26
  • The only issue remaining is that when I use `Navigator.push` and navigate away and then back to the HomeScreen, the data for the ListView disappears. Perhaps a StreamSubscription is required (as you outline here: https://stackoverflow.com/questions/54101589/navigating-to-a-new-screen-when-stream-value-in-bloc-changes)? I'm working on adding this, I'll post my result if successful. – Jack Jan 12 '20 at 19:07
  • Got it working. Posting the code below. Thanks again! – Jack Jan 12 '20 at 19:41

1 Answers1

2

Posting the working code. Thanks again to Remi for the tips!! Hope this code helps someone else!

class _HomeScreenState extends State<HomeScreen> {
  DatabaseReference usersChatsRef =
      FirebaseDatabase().reference().child('users-chats');

  List<UsersChats> myChats = [];

  StreamSubscription _streamSubscription;

  @override
  void initState() {
    super.initState();

    _streamSubscription = getData().listen((data) {

      setState(() {
        myChats = data;
      });
    });

  }

  @override
  void dispose() {
    _streamSubscription?.cancel(); // don't forget to close subscription
    super.dispose();
  }


  Stream<List<UsersChats>> getData() async* {
    var usersChatsStream = usersChatsRef.child('userId').onValue;
    var foundChats = List<UsersChats>();

    await for (var userChatSnapshot in usersChatsStream) {
      foundChats.clear();
      Map dictionary = userChatSnapshot.snapshot.value;
      if (dictionary != null) {
        for (var dictItem in dictionary.entries) {
          UsersChats thisChat;
          if (dictItem.key != null) {
            thisChat = UsersChats.fromMap(dictItem);
          } else {
            thisChat = UsersChats();
          }
          foundChats.add(thisChat);
        }
      }

      yield foundChats;
    }
  }

  @override
  Widget build(BuildContext context) {
    final user = Provider.of<User>(context);

    return Scaffold(
      appBar: AppBar(),
      body: ListView.builder(
        itemCount: myChats.length,
        itemBuilder: (context, index) {
            title: Text(myChats[index].id),
            subtitle: Text(myChats[index].lastUpdate),
            onTap: () {
              Navigator.pushNamed(context, MessagesScreen.id);
            },
          );
        },
      ),
    );
  }
}


Jack
  • 49
  • 5