Flutter : let’s know the ScrollController and ScrollNotification

Diego Velasquez
6 min readOct 23, 2018

--

In the majority of the apps we do, we have at least one Scrollable Widget.

In some cases we want to extend the use of our elements within a scroll, for example:

1- When we want to know if we reach at the beginning or end of a list. (Either vertical or horizontal).
2- When we want to move between elements of a list programmatically.
3- When we want to know if the scroll of our List started, is in progress or it ended.

We are going to review these 3 cases in detail.

If the list reached the minimum or maximum scroll

We start by building a simple app, which has a Text widget at the top that indicates when the minimum or maximum scroll was reached.
In the lower part we will have a list of elements.

Code:

String message = "";@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Scroll Limit reached"),
),
body: Column(
children: <Widget>[
Container(
height: 50.0,
color: Colors.green,
child: Center(
child: Text(message),
),
),
Expanded(
child: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
),
),
],
),
);
}

Once we have our widget, the first step is to declare a ScrollController variable.

ScrollController _controller;

We instantiate it within our initState method, in the following way:

@override
void initState() {
_controller = ScrollController();
super.initState();
}

Then we assign this _controller to our ListView.

ListView.builder(
controller: _controller,
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
)

With this we have our ListView connected with our ScrollController, we just have to listen to the events to determine what we want.

Listening to events

_scrollListener() {

}
@override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener);
super.initState();
}

Now we are listening to the Scroll events, but how we can know if the scroll reach the top or bottom.

We just have to add these validations:

_scrollListener() {
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
setState(() {
message = "reach the bottom";
});
}
if (_controller.offset <= _controller.position.minScrollExtent &&
!_controller.position.outOfRange) {
setState(() {
message = "reach the top";
});
}
}

We test by scrolling up or down and get this result

Result

That’s it, now we know when our list reached the top or bottom of the scroll, let’s see the other case.

Move between elements of a list.

We make a screen similar to the previous one, we add 2 separate buttons which we will use to move up or down between the elements of the list.
We also create a variable in which we define the size of our elements in the list, this variable itemSize we will use also to move that number of pixels up or down.

Code:

final itemSize = 100.0;_moveUp() {
//Add logic here
}
_moveDown() {
//Add logic here
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Scroll Movement"),
),
body: Column(
children: <Widget>[
Container(
height: 50.0,
color: Colors.green,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
RaisedButton(
child: Text("up"),
onPressed: _moveUp,
),
RaisedButton(
child: Text("down"),
onPressed: _moveDown,
)
],
),
),
),
Expanded(
child: ListView.builder(
itemCount: 30,
itemExtent: itemSize,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
),
),
],
),
);
}

Now we have to create our ScrollController and link it with our ListView

ScrollController _controller;@override
void initState() {
_controller = ScrollController();
super.initState();
}
...child: ListView.builder(
controller: _controller,
itemCount: 30,
itemExtent: itemSize,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
),

Once we have connected the ScrollController to our list, moving is very simple.

We can do it in 2 ways.

With animation:

When we want the scroll to have animation, it receives the offset in pixels as parameters , the Curve type animation , and the duration.

_controller.animateTo(pixelsToMove,
curve: Curves.linear, duration: Duration (milliseconds: 500));

Without animation:

When we want the scrolling without animation, it only receives the offset in pixels as parameter.

_controller.jumpTo(pixelsToMove);

For our example we are going to use the animated way since the user has to perceive the scrolling.

_moveUp() {
_controller.animateTo(_controller.offset - itemSize,
curve: Curves.linear, duration: Duration(milliseconds: 500));
}
_moveDown() {
_controller.animateTo(_controller.offset + itemSize,
curve: Curves.linear, duration: Duration(milliseconds: 500));
}

We use the current offset of our ScrollController which has the current pixels of its position, and we add or remove the itemSize which has the height of our current item from the list.

Result

Very simple, no? Yes, that’s right, that’s Flutter.

Finally we are going to review the last case.

Listen ScrollNotifications.

For this we create a UI similar to that of case one, in which we will display the state of our Scroll at the top.

Code:

String message = "";@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Scroll Status"),
),
body: Column(
children: <Widget>[
Container(
height: 50.0,
color: Colors.green,
child: Center(
child: Text(message),
),
),
Expanded(
child: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
),
),
],
),
);
}

Once we have that ready, we will need to use our NotificationListener Widget to capture notifications from our List.

So, first we add NotificationListener as a new container of our ListView and we will listen the events of type ScrollNotification,

Expanded(
child: NotificationListener<ScrollNotification>(

child: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
),
),
),

Now we’ll have implement the onNotification callback.
The code would look like this:

Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
//do your logic
},
child: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
),
),
),

In order to identify the type of ScrollNotification, we do it in the following way:

Expanded(
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollStartNotification) {
_onStartScroll(scrollNotification.metrics);
} else if (scrollNotification is ScrollUpdateNotification) {
_onUpdateScroll(scrollNotification.metrics);
} else if (scrollNotification is ScrollEndNotification) {
_onEndScroll(scrollNotification.metrics);
}
},
child: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return ListTile(title: Text("Index : $index"));
},
),
),
),

We created a method to update our top label.

_onStartScroll(ScrollMetrics metrics) {
setState(() {
message = "Scroll Start";
});
}
_onUpdateScroll(ScrollMetrics metrics) {
setState(() {
message = "Scroll Update";
});
}
_onEndScroll(ScrollMetrics metrics) {
setState(() {
message = "Scroll End";
});
}

Result

Super easy again, right?

Conclusion

As we have seen, using the ScrollController and ScrollNotification is not complicated. Also you can go deeper and play with ScrollPhysics and ScrollPosition .We can create very nice things if we want, like this: https://twitter.com/diegoveloper/status/1054400640795992064
You just have to let your imagination fly :)
And remember, with Flutter never say no to your designer.

You can check the source code in my flutter-samples repo https://github.com/diegoveloper/flutter-samples

References

https://docs.flutter.io/flutter/widgets/ScrollController-class.html
https://docs.flutter.io/flutter/animation/Curves-class.html
https://docs.flutter.io/flutter/widgets/ScrollNotification-class.html

--

--