Further to this, I had to “cheat” and look at the Appendix for the “step by step answers”…and still got it wrong.
Maybe it’s just me, but when working “step by step”, I work top to bottom through the code. So, following the steps in order, my code (copied from the appendix after using the IDE to convert the Stateless Widget into a Stateful Widget) looked like this:
class ExploreScreen extends StatefulWidget {
ExploreScreen({Key? key}) : super(key: key);
@override
State<ExploreScreen> createState() => _ExploreScreenState();
}
class _ExploreScreenState extends State<ExploreScreen> {
final mockService = MockFooderlichService();
late ScrollController _controller;
void _scrollListener() {
// 1
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
print('i am at the bottom!');
}
// 2
if (_controller.offset <= _controller.position.minScrollExtent &&
!_controller.position.outOfRange) {
print('i am at the top!');
}
}
@override
void initState() {
super.initState();
// 1
_controller = ScrollController();
// 2
_controller.addListener(_scrollListener);
}
@override
void dispose() {
_controller.removeListener(_scrollListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
...
This initially threw an error about the _controller
not being initialized.
So, I “cheated” further and looked at the final code from the book resources. And found this:
class ExploreScreen extends StatefulWidget {
const ExploreScreen({Key? key}) : super(key: key);
@override
_ExploreScreenState createState() => _ExploreScreenState();
}
class _ExploreScreenState extends State<ExploreScreen> {
final mockService = MockFooderlichService();
late ScrollController _controller;
@override
void initState() {
super.initState();
_controller = ScrollController();
_controller.addListener(_scrollListener);
}
void _scrollListener() {
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
print('reached the bottom');
}
if (_controller.offset <= _controller.position.minScrollExtent &&
!_controller.position.outOfRange) {
print('reached the top!');
}
}
@override
void dispose() {
_controller.removeListener(_scrollListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
...
Notice how the initState
and _scrollListener
sections are swapped?
While trying to recreate the error for this post, I could not, so now I am wondering if I was hit by a hot reload vs hot restart issue, but I wanted to note this a) in case someone else has the same issue, and b) to suggest this section be reworked slightly to clarify the order in which the code should be added if the order does, in fact, matter.
And, phrasing matters. In the Appendix description especially, but possibly in other areas, using phrasing like:
“Within the ExploreScreen
’s parent ListView
, all you have to do is set the scroll controller…”
(bolding added by me for emphasis)
Can make inexperienced readers feel like they should already know this.
I know the intent is to show how (theoretically) easy this is, but it can come across differently for folks who aren’t conversant with the topic yet.
(As the resident geek and former tech support person, I have been told this myself, which is why I mention it. I have now been on the receiving end.)
Hopefully all of this helps someone.