skip to Main Content

I am integrating a chat feature in my mobile application, and decided to use Firebase Realtime Database for the backend instad of Firestore as a cost reduction mechanism. I am running into a problem, however. There seems to be very sparse documentation on how to create infinite scrolling using data from Realtime Database instead of Firestore.

Below is the organization of my chat messages. This is the query I want to use:

FirebaseDatabase.instance
            .ref("messages/${widget.placeID}")
            .orderByChild("timeStamp")

And this is the widget I want to return for each result:

MessageWidget(
  message: message.text,
  id: message.uid,
  name: message.name,
  lastSender: message.lastSender,
  date: message.timeStamp,
  profilePicture: message.profilePicture,
);

Here is the database structure
enter image description here

The query works, and I have already programmed the MessageWidget from the JSON response of the query. All I need is for the query to be called whenever it reaches the top of its scroll, and load more MessageWdigets. Also note, this is a chat app where users are scrolling up, to load older messages, to be added above the previous.

Thank you!

EDIT: here is the code I currently have:

Flexible(
  child: StreamBuilder(
    stream: FirebaseDatabase.instance
        .ref("messages/${widget.placeID}")
        .orderByChild("timeStamp")
        .limitToLast(20)
        .onValue,
    builder:
        (context, AsyncSnapshot<DatabaseEvent> snapshot) {
      if (!snapshot.hasData) {
        return const CircularProgressIndicator();
      } else {
        Map<dynamic, dynamic> map =
            snapshot.data!.snapshot.value as dynamic;
        List<dynamic> list = [];
        list.clear();
        list = map.values.toList();
        return Align(
          alignment: Alignment.bottomCenter,
          child: Padding(
            padding: const EdgeInsets.only(bottom: 20),
            child: ListView.builder(
                controller: _scrollController,
                // shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: (context, index) {
                  final json = list[index]
                      as Map<dynamic, dynamic>;
                  final message = Message.fromJson(json);
                  return MessageWidget(
                    message: message.text,
                    id: message.uid,
                    name: message.name,
                    lastSender: message.lastSender,
                    date: message.timeStamp,
                    profilePicture:
                        message.profilePicture,
                  );
                }),
          ),
        );
      }
    },
  ),
),

My initState

void initState() {
   super.initState();

   _scrollController.addListener(() {
     if (_scrollController.position.atEdge) {
       bool isTop = _scrollController.position.pixels == 0;
       if (isTop) {
         //add more messages

       } else {
         print('At the bottom');
       }
     }
   });
 }

2

Answers


  1. Chosen as BEST ANSWER

    After several days of testing code, I came up with the following solution
    The first step is to declare a ScrollController in your state class.

    final ScrollController _scrollController = ScrollController();
    

    You will also need to declare a List to store query results

    List list = [];
    

    Next, use the following function to get initial data

      getStartData() async {
        //replace this with your path
        DatabaseReference starCountRef =
            FirebaseDatabase.instance.ref('messages/${widget.placeID}');
        starCountRef
            .orderByChild("timeStamp")
            //here, I limit my initial query to 6 results, change this to how many 
            //you want to load initially
            .limitToLast(6)
            .onChildAdded
            .forEach((element) {
          setState(() {
            list.add(element.snapshot.value);
            list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
          });
        });
      }
    

    Run this in initState

      void initState() {
        super.initState();
        FirebaseDatabase.instance.setPersistenceEnabled(true);
        getStartData();
      }
    

    Now to display the initial data that was generated when the page was loaded, build the results into a ListView

    ListView.builder(
                        itemCount: list.length,
                        controller: _scrollController,
                        //here I use a premade widget, replace MessageWidget with 
                        //what you want to load for each result
                        itemBuilder: (_, index) => MessageWidget(
                          message: list[index]["text"],
                          date: list[index]["timeStamp"],
                          id: list[index]["uid"],
                          profilePicture: list[index]["profilePicture"],
                          name: list[index]["name"],
                          lastSender: list[index]["lastSender"],
                        ),
                      ),
    

    Note that your ListView must be constrained, meaning that you can scroll to the beginning or end of your ListView. Sometimes, the ListView won't have enough data to fill and be scrollable, so you must declare a height with a Container or bound it to its contents.

    Now you have the code that fetches data when the page is loaded using getStartData() and initState. The data is stored in list, and a ListView.builder builds a MessageWidget for each item returned by getStartData. Now, you want to load more information when the user scrolls to the top.

      getMoreData() async {
        var moreSnapshots = await FirebaseDatabase.instance
            .ref("messages/${widget.placeID}")
            .orderByChild("timeStamp")
            .endBefore(list[0]["timeStamp"])
            .limitToLast(20)
            .once();
        var moreMap = moreSnapshots.snapshot.value as dynamic;
        setState(() {
          list.addAll(moreMap.values.toList());
          list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
        });
      }
    
    

    Then, make the function run when the ListView.builder is scrolled all the way to the top by adding this to the already existing initState.

        _scrollController.addListener(() {
          if (_scrollController.position.atEdge) {
            bool isTop = _scrollController.position.pixels == 0;
            if (isTop) {
              getMoreData();
            }
          }
        });
    

    Hopefully this helps or gives you a pointer on where to go from here. Thanks to Frank van Puffelen for his help on which query to use based on the previous answer.


  2. Your code already loads all messages.


    If you want to load a maximum number of messages, you’ll want to put a limit on the number of messages you load. If you want to load only the newest messages, you’d use limitToLast to do so – as the newest messages are last when you order them by their timeStamp value.

    So to load for example only the 10 latest messages, you’d use:

    FirebaseDatabase.instance
        .ref("messages/${widget.placeID}")
        .orderByChild("timeStamp")
        .limitToLast(10);
    

    This gives you the limited number of messages to initially show in the app.


    Now you need to load the 10 previous messages when the scrolling reaches the top of the screen. To do this, you need to know the timeStamp value and the key of the message that is at the top of the screen – so of the oldest message you’re showing so far.

    With those two values, you can then load the previous 10 with:

    FirebaseDatabase.instance
        .ref("messages/${widget.placeID}")
        .orderByChild("timeStamp")
        .endBefore(timeStampValueOfOldestSeenItem, keyOfOldestSeenItem)
        .limitToLast(10);
    

    The database here again orders the nodes on their timeStamp, it then finds the node that is at the top of the screen based on the values you give, and it then returns the 10 nodes right before that.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search