Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically scrolling to the end of a ListView

People also ask

How do I scroll to end of ListView in flutter?

position. maxScrollExtent; This simply means we are asking for the last position in the ListView via ScrollController.

How do I scroll to a specific position in ListView flutter?

All you have to do is set Global Keys for your widgets and call Scrollable. ensureVisible on the key of your widget you want to scroll to. For this to work your ListView should be a finite List of objects. If you are using ListView.


Use ScrollController.jumpTo() or ScrollController.animateTo() method to achieve this.

Example:

final _controller = ScrollController();

@override
Widget build(BuildContext context) {
  
  // After 1 second, it takes you to the bottom of the ListView
  Timer(
    Duration(seconds: 1),
    () => _controller.jumpTo(_controller.position.maxScrollExtent),
  );

  return ListView.builder(
    controller: _controller,
    itemCount: 50,
    itemBuilder: (_, __) => ListTile(title: Text('ListTile')),
  );
}

If you want smooth scrolling, then instead of using jumpTo above use

_controller.animateTo(
  _controller.position.maxScrollExtent,
  duration: Duration(seconds: 1),
  curve: Curves.fastOutSlowIn,
);

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

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


to get the perfect results I combined Colin Jackson and CopsOnRoad's answers as follows:

_scrollController.animateTo(
    _scrollController.position.maxScrollExtent,
    curve: Curves.easeOut,
    duration: const Duration(milliseconds: 500),
 );

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.