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
.