Flutter - How to use ScrollController to jumpto the bottom of the listview after fetching data from firebase using StreamBuilder?
Asked Answered
G

5

8

What is the best way to use ScrollController in the list for scrolling to the bottom of the list after the listview is rendered data from streambuilder using firestore query stream?

What is the best place to use scrollcontroller.jumpto method ?

// initialisation
ScrollController scrollController=ScrollController();

// inside initstate() and build method with 1000ms delay - not working
scrollController.jumpTo(scrollController.position.maxScrollExtent);

// widget inside build method
Expanded(
                child: StreamBuilder<DocumentSnapshot>(
                    stream: Firestore.instance
                        .collection('Data')
                        .document(widget.dataID)
                        .snapshots(),
                    builder: (context, AsyncSnapshot<DocumentSnapshot> snapshot) {
                      print("Called");
                      if (!snapshot.hasData) {
                        return Text("Loading");
                      }
                      var info= snapshot.data.data;
                      if (info!= null &&
                          info["messages"] != null &&
                          info["messages"].length > 0 &&
                          userList.length > 0) {
                        return ListView.builder(
                          controller: scrollController,
                          itemCount: info["challengeReplies"].length,
                          itemBuilder: (BuildContext context, int index) {

                            return ChallengeReplyListItem(
                                currentUserID: widget.currentUserID,
                                messagesType: info["messages"][index]["messagesType"],
                                messagesContent: info["messages"][index]["messagesContent"],
                          },
                        );
                      } else if (isLoading) {
                        return Center(
                          child: CircularProgressIndicator(),
                        );
                      } else {
                        return Center(child: Text("No Data Found",style: TextStyle(color: Colors.blueGrey,fontSize: 22,fontWeight: FontWeight.w500),));
                      }
                    }),
              ),

Can anyone suggest a proper solution to handle a scroll to the bottom of the page after the data gets rendered properly.

Thanks.

Glengarry answered 1/11, 2019 at 5:48 Comment(2)
Did you ever figure a solution?Northernmost
@Northernmost check my answer :)Mortise
A
3

Of course, the scrollcontroller is the first thing that comes to mind to make a chat go down to the last message sent. But it's not the only way to do it.

I provide a solution that I found when I was making a chat system:

StreamBuilder(
      stream: FirebaseFirestore.instance
      .collection('<MessagesColection>')
      .orderBy('<Time field>',descending: true)
      .snapshots(),
      builder: (context,snapshot) {                    
          return ListView.builder(
            //The reversed list will put the list backwards. 
            //The start of the list will start at the bottom.
            reverse: true, 
            controller: scrollController,      
            itemCount: snapshot.data.size,
            itemBuilder: (context, index) {                                                    
              return ChatMessage(snapshot);          
            },
          );
        }
    ),

In the previous code, what was done was to invert the list, the most recent messages will be at the bottom, and order the records in descending order, that is, from the most recent to the least recent. In this way the most recent messages will be at the beginning of the list (in this case at the bottom), and the least at the bottom of the list (in this case at the top).

Adipose answered 25/11, 2020 at 0:57 Comment(1)
nice! descending: true works as we are reversing it in the listviewAudacious
S
3

use scrollController.jumpTo with addPostFrameCallback if snapshot already has data

below is my code snippet

StreamBuilder<List<ConversationModel>>(
                            stream:
                                convoService.getConversation(conversationsPath),
                            builder: (context, snapshot) {
                              if (snapshot.hasData) {
                                final List<ConversationModel> convoList =
                                    snapshot.data ?? [];

                                SchedulerBinding.instance
                                    ?.addPostFrameCallback((_) {
                                  scrollController.jumpTo(
                                    scrollController.position.maxScrollExtent,
                                  );
                                });

                                return buildChatList(convoList);
                              } else {
                                return buildLoader(context);
                              }
                            },
                          )
Siblee answered 19/7, 2022 at 11:43 Comment(1)
This works pretty well. Thank you.Gaylegayleen
C
0
scrollController.jumpTo(scrollController.position.maxScrollExtent);

Will only scroll the list view to maxScrollValue, i.e., maximum scroll possible for one stroke. You will have to use

scrollController.jumpTo(info["challengeReplies"].length*1.0)

to achieve what you are trying.

Reason for info["challengeReplies"].length*1.0: Coz, JumpTo needs a double value.

Collegian answered 12/4, 2020 at 20:22 Comment(0)
M
0

This may be a little hacky, but I couldn't find another way.

In my case (I was using PageView inside StreamBuilder, but should be similar to yours),

I extracted it into separate stateful widget, gave it a UniqueKey() and moved its ScrollController into its state, like(Please read comments in the code):

  //The extracted scrolling widget 
  class ScrollHeader extends StatefulWidget {
      const ScrollHeader({
        Key key,
        @required this.slides,
        @required this.currentSlide,
      }) : super(key: key);
    
      final List<Widget> slides;
      final int currentSlide;
    
      @override
      _ScrollHeaderState createState() => _ScrollHeaderState();
    }
    
    class _ScrollHeaderState extends State<ScrollHeader> {
      PageController headerController;
    
      @override
      void initState() {
        super.initState();
    
        //Assign initialPage(Or newly received page) in initState.
        headerController = PageController(initialPage: widget.currentSlide);
      }
    
      @override
      Widget build(BuildContext context) {

        //Since PageView doesn't exist on the first render yet, 
        //ScrollController will throw an error, so we need to escape it using IF:
        if (headerController.hasClients) {
          headerController.animateToPage(
            widget.currentSlide,
            duration: Duration(milliseconds: 350),
            curve: Curves.easeIn,
          );
        }

        //If you don't need animation, you can just do this without IFs:
        headerController.jumpToPage(index);
    
        return PageView(
          scrollDirection: Axis.horizontal,
          children: widget.slides,
          controller: headerController,
        );
      }
    }

The StreamBuilder itself will provide this Scrolling widget with initialPage and with child elements:

Widget build(BuildContext context) {

    Key appbarKey = UniqueKey(); //We need Key to not destroy State between StreamBuilder's updates 

    StreamBuilder(
                    stream: appBarState.stream$, //Could be any stream(Like Firestore for example).
                    builder: (context, snap) {
                      if (snap.hasData) {
                        //Just generated children elements
                        List<Widget> slides = List<Widget>.from(
                          List<String>.from(appBarState.current["slides"]).map(
                            (e) => Center(
                              child: Text(
                                e,
                                style: TextStyle(
                                  color: theme.accentColor,
                                  fontSize: 20,
                                ),
                              ),
                            ),
                          ),
                        );
                        print(snap.data);
                        return ScrollHeader(
                          key: appbarKey,
                          slides: slides,
                          currentSlide: snap.data["currentSlide"], //This is the page that pageView will snap/animate to after receiving new data
                        );
                      }
                      return Container();
                    },
                  ),

This allows you to update and animate/snap pages of Scrolling widget at any time as well as update its children when using streams.

Hope that helps, and I hope someone will post a better and more elegant solution, but this one works(So far).

Mortise answered 25/2, 2021 at 9:50 Comment(0)
H
0

try this it will work:

StreamBuilder(
  stream: ...,
  builder: (BuildContext context, AsyncSnapshot snapshot) {
    // Like this:
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (_controller.hasClients) {
        _controller.jumpTo(_controller.position.maxScrollExtent);
      } else {
        setState(() => null);
      }
     });

     return PutYourListViewHere
}),
Hindoo answered 6/12, 2021 at 6:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.