Programmatically scrolling to the end of a ListView
Asked Answered
P

22

246

I have a scrollable ListView where the number of items can change dynamically. Whenever a new item is added to the end of the list, I would like to programmatically scroll the ListView to the end. (e.g., something like a chat message list where new messages can be added at the end)

My guess is that I would need to create a ScrollController in my State object and pass it manually to the ListView constructor, so I can later call animateTo() / jumpTo() method on the controller. However, since I cannot easily determine the maximum scroll offset, it seems impossible to simply perform a scrollToEnd() type of operation (whereas I can easily pass 0.0 to make it scroll to the initial position).

Is there an easy way to achieve this?

Using reverse: true is not a perfect solution for me, because I would like the items to be aligned at the top when there are only a small number of items that fit within the ListView viewport.

Patterman answered 19/4, 2017 at 2:55 Comment(0)
S
175

If you use a shrink-wrapped ListView with reverse: true, scrolling it to 0.0 will do what you want.

import 'dart:collection';

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Example',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Widget> _messages = <Widget>[new Text('hello'), new Text('world')];
  ScrollController _scrollController = new ScrollController();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new Container(
          decoration: new BoxDecoration(backgroundColor: Colors.blueGrey.shade100),
          width: 100.0,
          height: 100.0,
          child: new Column(
            children: [
              new Flexible(
                child: new ListView(
                  controller: _scrollController,
                  reverse: true,
                  shrinkWrap: true,
                  children: new UnmodifiableListView(_messages),
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        child: new Icon(Icons.add),
        onPressed: () {
          setState(() {
            _messages.insert(0, new Text("message ${_messages.length}"));
          });
          _scrollController.animateTo(
            0.0,
            curve: Curves.easeOut,
            duration: const Duration(milliseconds: 300),
          );
        }
      ),
    );
  }
}
Schopenhauer answered 19/4, 2017 at 4:28 Comment(12)
This did the trick. In my specific case I had to use nested Columns in order to put the ListView in an Expanded widget, but the basic idea is the same.Patterman
I came searching for a solution, just wanted to mention that the other answer may be a better way to achieve this - #44141648Hbeam
I agree, that answer (which I also wrote) definitely better.Schopenhauer
Why do we have to enable the reverse as true?Newspaperman
@Dennis So that the ListView starts in reverse order that is from the bottom.Dragonnade
ShrinkWrap can be dangerous to set docs.flutter.io/flutter/widgets/ScrollView/shrinkWrap.html "Shrink wrapping the content of the scroll view is significantly more expensive than expanding to the maximum allowed size because the content can expand and contract during scrolling" I was able to achieve the same thing with scrollController.animateTo(scrollController.position.maxScrollExtent, duration: Duration(milliseconds: 300), curve: Curves.easeOut)Classmate
I'm removing this code from my project because it gaves me some headaches. Try creating a scaffold project with a list of messages and then create a Timer.periodic within every message widget to count an int variable. Then add a message to the list using insert(0, message) and you'll see a bug: The first message counter will reset and the last message sent now will be with the counter from the first message. And not, this is not best answer. Try @Dragonnade answer. His answer is better than this.Cyclorama
To get last item in Listview you might want to add extra bias to controller controller.jumpTo(_controller.position.maxScrollExtent+bias);Boozer
My answer would work with you perfectly (https://mcmap.net/q/116133/-programmatically-scrolling-to-the-end-of-a-listview)Phreno
How can I get this to work with reverse set to false?Gwen
I want to scroll automatically without any click but it says controller is not attached to any view. how can I know if there is data loaded in list view builderHurley
We need to wrap it inside WidgetsBinding.instance.addPostFrameCallback() as mentioned here: https://mcmap.net/q/116133/-programmatically-scrolling-to-the-end-of-a-listview to get correct positioning for dynamic lists.Nevile
D
303

Screenshot:

enter image description here

  1. Scrolling with animation:

    final ScrollController _controller = ScrollController();
    
    // This is what you're looking for!
    void _scrollDown() {
      _controller.animateTo(
        _controller.position.maxScrollExtent,
        duration: Duration(seconds: 2),
        curve: Curves.fastOutSlowIn,
      );
    }
    
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        floatingActionButton: FloatingActionButton.small(
          onPressed: _scrollDown,
          child: Icon(Icons.arrow_downward),
        ),
        body: ListView.builder(
          controller: _controller,
          itemCount: 21,
          itemBuilder: (_, i) => ListTile(title: Text('Item $i')),          
        ),
      );
    }
    
  2. Scrolling without animation:

    Replace above _scrollDown method with this:

    void _scrollDown() {
      _controller.jumpTo(_controller.position.maxScrollExtent);
    }
    
Dragonnade answered 29/10, 2018 at 11:53 Comment(7)
For Firebase Realtime Database, I have so far been able to get away with 250ms (which is probably really long). I wonder how low we can go? The problem is maxScrollExtent needs to wait for the widget build to finish with the new item added. How to profile the average duration from callback to build completion?Governorship
Do you think this will work properly with using streambuilder for fetching data from firebase? Also adding chat message on send button as well? Also if internet connection is slow will it work? anything better if you can suggest. Thanks.Moraceous
@Governorship what was your approach with this issue plus firebase ?Thora
@IdrisStack Actually, 250ms is evidently not long enough as sometimes, the jumpTo happens before the update occurs. I think the better approach would be to compare list length after update and jumpTo the bottom if the length is one greater.Governorship
Thanks @SacWebDeveloper, your approach works, maybe can I ask you another question in the context of Firestore, it might divert the topic abit. Qn. Why is it that when I send a message to someone the list doesn't automatically scroll to bottom, is there a way to listen the message and scroll to bottom ?Thora
@IdrisStack You'll need to make a new question on SO with your code. It could be too many possible reasons. Does the list update at all? Make sure you are subscribed to child added. Also, you don't need to wait for the server to display and scroll to your own messages. Just update locally.Governorship
hope is not a strategy: system may delay execution of your app for as long as it pleases. You should perform this action in a post frame callback as explained in the other answer: https://mcmap.net/q/116133/-programmatically-scrolling-to-the-end-of-a-listviewFelsite
S
175

If you use a shrink-wrapped ListView with reverse: true, scrolling it to 0.0 will do what you want.

import 'dart:collection';

import 'package:flutter/material.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Example',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Widget> _messages = <Widget>[new Text('hello'), new Text('world')];
  ScrollController _scrollController = new ScrollController();

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new Container(
          decoration: new BoxDecoration(backgroundColor: Colors.blueGrey.shade100),
          width: 100.0,
          height: 100.0,
          child: new Column(
            children: [
              new Flexible(
                child: new ListView(
                  controller: _scrollController,
                  reverse: true,
                  shrinkWrap: true,
                  children: new UnmodifiableListView(_messages),
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        child: new Icon(Icons.add),
        onPressed: () {
          setState(() {
            _messages.insert(0, new Text("message ${_messages.length}"));
          });
          _scrollController.animateTo(
            0.0,
            curve: Curves.easeOut,
            duration: const Duration(milliseconds: 300),
          );
        }
      ),
    );
  }
}
Schopenhauer answered 19/4, 2017 at 4:28 Comment(12)
This did the trick. In my specific case I had to use nested Columns in order to put the ListView in an Expanded widget, but the basic idea is the same.Patterman
I came searching for a solution, just wanted to mention that the other answer may be a better way to achieve this - #44141648Hbeam
I agree, that answer (which I also wrote) definitely better.Schopenhauer
Why do we have to enable the reverse as true?Newspaperman
@Dennis So that the ListView starts in reverse order that is from the bottom.Dragonnade
ShrinkWrap can be dangerous to set docs.flutter.io/flutter/widgets/ScrollView/shrinkWrap.html "Shrink wrapping the content of the scroll view is significantly more expensive than expanding to the maximum allowed size because the content can expand and contract during scrolling" I was able to achieve the same thing with scrollController.animateTo(scrollController.position.maxScrollExtent, duration: Duration(milliseconds: 300), curve: Curves.easeOut)Classmate
I'm removing this code from my project because it gaves me some headaches. Try creating a scaffold project with a list of messages and then create a Timer.periodic within every message widget to count an int variable. Then add a message to the list using insert(0, message) and you'll see a bug: The first message counter will reset and the last message sent now will be with the counter from the first message. And not, this is not best answer. Try @Dragonnade answer. His answer is better than this.Cyclorama
To get last item in Listview you might want to add extra bias to controller controller.jumpTo(_controller.position.maxScrollExtent+bias);Boozer
My answer would work with you perfectly (https://mcmap.net/q/116133/-programmatically-scrolling-to-the-end-of-a-listview)Phreno
How can I get this to work with reverse set to false?Gwen
I want to scroll automatically without any click but it says controller is not attached to any view. how can I know if there is data loaded in list view builderHurley
We need to wrap it inside WidgetsBinding.instance.addPostFrameCallback() as mentioned here: https://mcmap.net/q/116133/-programmatically-scrolling-to-the-end-of-a-listview to get correct positioning for dynamic lists.Nevile
M
32

listViewScrollController.animateTo(listViewScrollController.position.maxScrollExtent) is the simplest way.

Mordent answered 26/9, 2019 at 8:48 Comment(4)
Hi Jack, thanks for your answer, I believe I wrote the same, so there is no advantage of adding same solution again IMHO.Dragonnade
I was using this approach but you need to wait for some ms to do it because the screen needs to be rendered before running the animateTo(...). Maybe we've a nice way using good practices to do it without a hack.Cyclorama
That is the right answer. It will scroll your list to bottom if you +100 in current line. like listViewScrollController.position.maxScrollExtent+100;Lyndonlyndsay
With inspiration from @RenanCoelho, I wrapped this line of code in a Timer with delay of 100 milliseconds.Ingemar
A
30

To get the perfect results I combined Colin Jackson and CopsOnRoad's answers. Use .position.maxScrollExtent:

_scrollController.animateTo(
    _scrollController.position.maxScrollExtent,
    curve: Curves.easeOut,
    duration: const Duration(milliseconds: 500),
 );
Ammamaria answered 7/7, 2020 at 4:50 Comment(1)
Can this be used if I need my list reversed? Or how can I modify it to work?Gwen
U
20

While all the answers produces the desired effects we should do some improvements here.

  • First of all in most cases (speaking about auto scrolling) is useless using postFrameCallbacks because some stuff could be rendered after the ScrollController attachment (produced by the attach method), the controller will scroll until the last position that he knows and that position could not be the latest in your view.

  • Using reverse:true should be a good trick to 'tail' the content but the physic will be reversed so when you try to manually move the scrollbar you must move it to the opposite side -> BAD UX.

  • Using timers is a very bad practice when designing graphic interfaces -> timer are a kind of virus when used to update/spawn graphics artifacts.

Anyway speaking about the question the right way to accomplish the task is using the jumpTo method with the hasClients method as a guard.

Whether any ScrollPosition objects have attached themselves to the ScrollController using the attach method. If this is false, then members that interact with the ScrollPosition, such as position, offset, animateTo, and jumpTo, must not be called

Speaking in code simply do something like this:

if (_scrollController.hasClients) {
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}

Anyway this code is still not enough, the method will be triggered even when the scrollable isn't at the end of the screen so if you are manually moving the bar the method will triggered and autoscrolling will be performed.

We ca do better, with the help of a listener and a couple of bool will be fine.
I'm using this technique to visualize in a SelectableText the value of a CircularBuffer of size 100000 and the content keeps updating correctly, the autoscroll is very smooth and there are not performance issues even for very very very long contents. Maybe as someone said in other answers the animateTo method could be smoother and more customizable so feel free to give a try.

  • First of all declare these variables:
ScrollController _scrollController = new ScrollController();
bool _firstAutoscrollExecuted = false;
bool _shouldAutoscroll = false;
  • Then let's create a method for autoscrolling:
void _scrollToBottom() {
    _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
  • Then we need the listener:
void _scrollListener() {
    _firstAutoscrollExecuted = true;

    if (_scrollController.hasClients && _scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        _shouldAutoscroll = true;
    } else {
        _shouldAutoscroll = false;
    }
}
  • Register it in initState:
@override
void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
}
  • Remove the listener in your dispose:
@override
void dispose() {
    _scrollController.removeListener(_scrollListener);
    super.dispose();
}
  • Then trigger _scrollToBottom, basing on your logic and needs, in your setState:
setState(() {
    if (_scrollController.hasClients && _shouldAutoscroll) {
        _scrollToBottom();
    }

    if (!_firstAutoscrollExecuted && _scrollController.hasClients) {
         _scrollToBottom();
    }
});

EXPLANATION

  • We made a simple method: _scrollToBottom() in order to avoid code repetitions;
  • We made a _scrollListener() and we attached it to the _scrollController in the initState -> will be triggered after the first time that the scrollbar will move. In this listener we update the value of the bool value _shouldAutoscroll in order to understand if the scrollbar is at the bottom of the screen.
  • We removed the listener in the dispose just to be sure to not do useless stuff after the widget dispose.
  • In our setState when we are sure that the _scrollController is attached and that's at the bottom (checking for the value of shouldAutoscroll) we can call _scrollToBottom().
    At the same time, only for the 1st execution we force the _scrollToBottom() short-circuiting on the value of _firstAutoscrollExecuted.
Uphold answered 14/4, 2021 at 17:7 Comment(2)
Tks for the time to give a proper explanation and provide the code.Canadian
This properly explain it, thanks for this.Autotruck
T
11

Do not put the widgetBinding in the initstate, instead, you need to put it in the method that fetches your data from database. for example, like this. If put in initstate, the scrollcontroller will not attach to any listview.

    Future<List<Message>> fetchMessage() async {

    var res = await Api().getData("message");
    var body = json.decode(res.body);
    if (res.statusCode == 200) {
      List<Message> messages = [];
      var count=0;
      for (var u in body) {
        count++;
        Message message = Message.fromJson(u);
        messages.add(message);
      }
      WidgetsBinding.instance
          .addPostFrameCallback((_){
        if (_scrollController.hasClients) {
          _scrollController.jumpTo(_scrollController.position.maxScrollExtent);
        }
      });
      return messages;
    } else {
      throw Exception('Failed to load album');
    }
   }
Takeoff answered 27/9, 2020 at 7:50 Comment(1)
I'd put TL;DR WidgetsBinding.instance.addPostFrameCallback((_){_scrollController.jumpTo(_scrollController.position.maxScrollExtent);}) at the top of this answer, as the surrounding code is confusing.Felsite
I
7
_controller.jumpTo(_controller.position.maxScrollExtent);
_controller.animateTo(_controller.position.maxScrollExtent);

These calls do not work well for a list of dynamically sized items. We don't know at the time that you call jumpTo() how long the list is, since all of the items are variable and are lazily built as we scroll down the list.

This may not be the smart way, but as a last resort you can do the following:

Future scrollToBottom(ScrollController scrollController) async {
  while (scrollController.position.pixels != scrollController.position.maxScrollExtent) {
    scrollController.jumpTo(scrollController.position.maxScrollExtent);
    await SchedulerBinding.instance!.endOfFrame;
  }
}
Immunochemistry answered 16/5, 2021 at 20:43 Comment(0)
G
6

I came across this issue when I was using the StreamBuilder widget to get data from my database. I put WidgetsBinding.instance.addPostFrameCallback on top of the widget's build method, and it wouldn't scroll all the way to the end. I fixed it by doing this:

...
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
}),
...

I tried it with _controller.animateTo too but it didn't seem to work.

Gramercy answered 10/1, 2021 at 12:31 Comment(0)
N
5

if you want to see the last item visible with a padding from bottom then add extra distance like this

 _controller.jumpTo(_controller.position.maxScrollExtent + 200);

here 200 will be extra distance

Nanceynanchang answered 28/4, 2022 at 13:44 Comment(0)
S
4

I was using a dynamic list view, but scrollController.animateTo() won't work for dynamic lists as mentioned here https://mcmap.net/q/116133/-programmatically-scrolling-to-the-end-of-a-listview, and I didn't even find any good solution in previous replies. So here's how I solved the issue.

void scrollToMaxExtent() {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    scrollController.animateTo(
      scrollController.position.maxScrollExtent,
      duration: const Duration(milliseconds: 100),
      curve: Curves.easeIn,
    );
  });
}
Soapbox answered 8/4, 2022 at 9:36 Comment(1)
This is how I resolved it as well, by far this is the best solution one for my needs.Nevile
P
3

depending on this answer I have created this class, just send your scroll_controller, and if you want the opposite direction use the reversed parameter

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

class ScrollService {
  static scrollToEnd(
      {required ScrollController scrollController, reversed = false}) {
    SchedulerBinding.instance!.addPostFrameCallback((_) {
      scrollController.animateTo(
        reverced
            ? scrollController.position.minScrollExtent
            : scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
    });
  }
}
Phreno answered 30/8, 2021 at 11:51 Comment(1)
This method worked well on blocks using "Visiblity", great tip! Thank youPerrault
C
2

I have so much problem trying to use the scroll controller to go to the bottom of the list that I use another approach.

Instead of creating an event to send the list to the bottom, I change my logic to use a reversed list.

So, each time I have a new item, I simply, made at insert at the top of the list.

// add new message at the begin of the list 
list.insert(0, message);
// ...

// pull items from the database
list = await bean.getAllReversed(); // basically a method that applies a descendent order

// I remove the scroll controller
new Flexible(
  child: new ListView.builder(
    reverse: true, 
    key: new Key(model.count().toString()),
    itemCount: model.count(),
    itemBuilder: (context, i) => ChatItem.displayMessage(model.getItem(i))
  ),
),
Confectionary answered 25/1, 2019 at 10:7 Comment(1)
The only problem that I'm having with this is that any scroll bars work backwards now. When I scroll down , they move up.Salvador
S
2

My solution :

Step : 1 Define global key like this :

final lastKey = GlobalKey();

Step : 2 Attach to last message

SingleChildScrollView(
    controller: scrollController,
    padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
    physics: const AlwaysScrollableScrollPhysics(),
    child: Column(
        children: List.generate(
            data.length,
            (index) {
            return BuildMessage(
                key:
                    data.length == index + 1 ? lastKey : null,
                message: data[index],
                focusNode: focusNode,
            );
            },
        ),
    ),
)

Step : 3 Create function call to scroll

void scrollToBottom() {
    Scrollable.ensureVisible(lastKey.currentContext!,alignment: 1, duration: const Duration(milliseconds: 500));
}

Call when you want to scroll to bottom with some delay 100 milliseconds

Timer(const Duration(milliseconds: 100),() => scrollToBottom());
Subsistent answered 21/2, 2022 at 2:38 Comment(2)
Will it work If the last item has not even been rendered?Dol
@HensonFang yes SingleChildScrollView render all items at onceShawntashawwal
J
2

I add one more thing: ScrollController only runs on Emulator or physical device, so if you debugging on window application or browser will not take effect. I debug on a windows app that didn't work until I figure out it only runs on Emulator and physical device. Hope this save your time.

Update: ScrollController can be using in window application, I have checked it work.

Jarietta answered 4/4, 2023 at 4:6 Comment(0)
K
1

Pros:

Here is the solution that works 100% of the time. It always scrolls to the end, unlike some of the other solutions. Also, it doesn't require reversing.

Cons:

Locked to Curves.linear animation curve.

    Future.doWhile(() {
      if (scrollController.position.extentAfter == 0)
        return Future.value(false);
      return scrollController
          .animateTo(scrollController.position.maxScrollExtent,
              duration: Duration(milliseconds: 100), curve: Curves.linear)
          .then((value) => true);
    });
Kabob answered 19/10, 2022 at 16:8 Comment(0)
R
1

First declare scrollController like this:

ScrollController _controller = ScrollController();

and also import

import 'package:cloud_firestore/cloud_firestore.dart' as cf;

If you are using Streambuilder then this code may help you:

StreamBuilder(
    stream: FirebaseFirestore.instance
        .collection("chatRoom")
        .doc(Get.arguments['chatRoomId'])
        .collection("chats")
        .orderBy('time',descending: false)
        .snapshots(),
    builder: (BuildContext context, AsyncSnapshot<cf.QuerySnapshot> snapshot){
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (_controller.hasClients) {
          _controller.jumpTo(_controller.position.maxScrollExtent);
        } else {
          setState(() => null);
        }
      });
        return ListView(
          controller: _controller,
          children: snapshot.data!.docs.map((document) {
            return Center(
              child:Text(document['message'])  //field from firestore 
            );
          }).toList(),
        );
    }
);

If you are just using listview then this will help you:

        return ListView(
          controller: _controller,
          children: list.length {
            if (_controller.hasClients) {
              _controller.jumpTo(_controller.position.maxScrollExtent);
            }
            return Center(
              child:Text(list[index)  
            );
          }),
        );
    }
);
Rhearheba answered 8/6, 2023 at 7:38 Comment(0)
L
1

Issue

If your attempt at scrolling to the bottom of a scroll view isn't working as expected, it's likely due to the underlying ScrollController not knowing that the "maximum scroll extent" is. This is because lists can be lazily loaded, meaning children are only constructed when they're needed.

This presents itself as the following code snippet stopping early:

ScrollController.position.jumpTo(ScrollController.position.maxScrollExtent)

Solution

You can use an AnimationController to drive your ScrollController for moving to the bottom of your scroll view.

  1. Add a listener callback to your AnimationController so that it updates your ScrollController such that the animation controller's value (zero to one) corresponds to the scroll controller's scroll position (zero to maxScrollExtent).
  2. Drive your animation forward or backward as needed (in this example I'm using fling() with a velocity of 1.0 for going down and -1.0 for going up, but you may also directly set the value to any double between 0 and 1).
  3. Provide the ScrollController to your scroll view.

Then, whenever you add a new item to your list, you can simply update your AnimationController to scroll to the bottom.

void addListItem() {
  setState(() => items.add(newItem));
  WidgetsBinding.instance.addPostFrameCallback((_) {
    // Doing this in the next frame ensures your list view has rendered with the new item
    animationController.fling();
  });
}

Dartpad example

In the example below, the two text buttons at the top illustrate scrolling to the bottom and the two text buttons at the bottom illustrate scrolling to the top. Notice how scrolling to the bottom via "Bad scroll to bottom" stops early, but "Good scroll to bottom" completes successfully.

import 'package:flutter/material.dart';

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: LazyList(),
      ),
    );
  }
}

class LazyList extends StatefulWidget {
  @override
  State<LazyList> createState() => _LazyListState();
}

class _LazyListState extends State<LazyList> with SingleTickerProviderStateMixin {
  late final _animationController = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 3),
  );

  final _scrollController = ScrollController();
  
  @override
  void initState() {
    super.initState();
    _animationController.addListener(_linkAnimationToScroll);
  }
  
  @override
  void dispose() {
    _animationController.dispose();
    _scrollController.dispose();
    super.dispose();
  }
  
  /// Joins the `AnimationController` to the `ScrollController`, providing ample
  /// time for the lazy list to render its contents while scrolling to the bottom.
  void _linkAnimationToScroll() {
    _scrollController.jumpTo(
      _animationController.value * _scrollController.position.maxScrollExtent,
    );
  }
  
  /// Relying on just the `ScrollController` will fail because the cache extent
  /// is not long enough for its max position to be correctly computed.
  void _badScrollToBottom() {
    _scrollController.position.animateTo(
      _scrollController.position.maxScrollExtent,
      duration: const Duration(milliseconds: 500),
      curve: Curves.fastEaseInToSlowEaseOut,
    );
  }
  
  /// Utilizings the link between the `AnimationController` and `ScrollController`
  /// to start at the user's current scroll position and fling them to the bottom.
  ///
  /// ("bottom" is the max scroll extent seen in [_linkAnimationToScroll])
  void _goodScrollToBottom() {
    _animationController.value = _scrollController.position.pixels / _scrollController.position.maxScrollExtent;
    _animationController.fling();
  }
  
  /// This is fine because the `ScrollController` already knows where 0 is.
  void _goodScrollToTop() {
    _scrollController.position.animateTo(
      0,
      duration: const Duration(milliseconds: 500),
      curve: Curves.fastEaseInToSlowEaseOut,
    );
  }
  
  /// This is also good because the `AnimationController` is wired up to the `ScrollController`.
  void _otherGoodScrollToTop() {
    _animationController.value = _scrollController.position.pixels / _scrollController.position.maxScrollExtent;
    _animationController.fling(velocity: -1);
  }
  
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      controller: _scrollController,
      slivers: [
        const SliverAppBar(
          title: Text('Scroll to bottom demo'),
          centerTitle: false,
        ),
        SliverToBoxAdapter(
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextButton(
                onPressed: _badScrollToBottom,
                child: const Text('Bad scroll to bottom'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: _goodScrollToBottom,
                child: const Text('Good scroll to bottom'),
              ),
            ],
          ),
        ),
        SliverList.list(
          children: List.generate(
            50,
            (i) {
              return Container(
                alignment: Alignment.centerLeft,
                height: i * 20,
                child: Text('$i'),
              );
            },
          ),
        ),
        SliverToBoxAdapter(
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextButton(
                onPressed: _goodScrollToTop,
                child: const Text('Good scroll to top'),
              ),
              const SizedBox(width: 8),
              TextButton(
                onPressed: _otherGoodScrollToTop,
                child: const Text('Other good scroll to top'),
              ),
            ],
          ),
        ),
      ],
    );
  }
}
Lave answered 25/9, 2023 at 21:53 Comment(0)
F
0

For me the issue was that scrollController.position.maxScrollExtent always returned 0.0. The reason was that my ListView was inside a ScrollView.

Removing the ScrollView fixed the issue.

Folkestone answered 26/6, 2022 at 20:43 Comment(0)
M
0

According to https://github.com/flutter/flutter/issues/71742 "I believe this is expected behavior for a list of dynamically sized items. We don't know at the time that you call jumpTo how long the list is, since all of the sizes are variable and are lazily built as we scroll down the list."

If you want your listview to scroll to the bottom of the screen you can write this:

final ScrollController _scrollController = ScrollController();
_scrollController.animateTo(
                0x7fffffff, //Max int
                duration: const Duration(seconds: 2),
                curve: Curves.fastOutSlowIn,
              );

The problem with this approach is that it does not animate well, you can play around with the larger numbers to see if that helps with the animation or use this package https://pub.dev/packages/scrollable_positioned_list recommended in the same github issue thread.

Masque answered 17/3, 2023 at 15:21 Comment(0)
H
0

The issue you're facing is likely because your list is not fully rendered when trying to scroll to the end.

You call the scroll method after adding elements to your list and changing the state. At that point, the Flutter framework might not have completed the rendering of your new items, so when you call scrollController.position.maxScrollExtent, it doesn't include the new items you've just added.

A possible solution is to add a PostFrameCallback, which will be executed after Flutter completes the current frame and all elements are rendered. You can use

WidgetsBinding.instance.addPostFrameCallback(_) to achieve this.

Here is how you can adjust your code (You can add this method in the function that updates your list. No need to specifically include it in the initState):

WidgetsBinding.instance.addPostFrameCallback((_) {
          scrollController.animateTo(
            scrollController.position.maxScrollExtent,
            duration: const Duration(seconds: 1),
            curve: Curves.fastOutSlowIn,
          );
        });
Hectare answered 14/5, 2023 at 9:21 Comment(0)
L
0

If you add a new element just before scrolling, you may encouter unexpected behaviour, to solve that, add a tiny Future.delayed before scrolling:

      Future.delayed(Duration(milliseconds: 100)).then((_){
        scrollController.animateTo(
          scrollController.position.maxScrollExtent,
          duration: Duration(seconds: 1),
          curve: Curves.fastOutSlowIn,
        );
      });
Lineal answered 10/8, 2023 at 21:25 Comment(0)
C
-3

you could use this where 0.09*height is the height of a row in the list and _controller is defined like this _controller = ScrollController();

(BuildContext context, int pos) {
    if(pos != 0) {
        _controller.animateTo(0.09 * height * (pos - 1), 
                              curve: Curves.easeInOut,
                              duration: Duration(milliseconds: 1400));
    }
}
Contraindicate answered 26/5, 2020 at 15:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.