Why using a StatefulWidget to implement the first challenge solution in Chapter 5?

Is there any reason to use a StatefullWidget to implement the solution for the first challenge in Chapter 5?

I have simply used a local variable for ScrollController in the build() method of the ExploreScreen StatelessWidget and it worked perfectly, but as I’m new to Flutter I’m not sure of the tradeoffs of this solution compared to the book solution.

Could anyone give an explanation, please?

this is how my solution look:

class ExploreScreen extends StatelessWidget {

  final fooderlichService = MockFooderlichService();

  ExploreScreen({Key? key}) : super(key: key);

  @override

  Widget build(BuildContext context) {
    final scrollController = ScrollController();
    scrollController.addListener(() {
      final minPos = scrollController.position.minScrollExtent;
      final maxPos = scrollController.position.maxScrollExtent;
      if (scrollController.offset == minPos) {
        print("I'm at the top");
      }
      if (scrollController.offset == maxPos) {
        print("I'm at the bottom");
      }
    });

    return FutureBuilder(
      future: fooderlichService.getExploreData(),
      builder: (context, AsyncSnapshot<ExploreData> snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          final recipes = snapshot.data?.todayRecipes ?? [];
          final posts = snapshot.data?.friendPosts ?? [];
          return ListView(
            controller: scrollController,
            children: [
              TodayRecipeListView(recipes: recipes),
              FriendPostListView(posts: posts),
            ],
          );
        } else {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    );
  }
}
1 Like

@alilosoft if you do this make sure you dispose your scrollController.

Placing the scrollController within the build method itself will create a new instance, and add a brand new listener to the scroll controller. So you will have many scroll controllers.

Good idea to only have one, and dispose the controller when the widget is destroyed.

3 Likes

I’m coming from a Java background, and as far as I know in Java the Garbage collector will take care of unrefrenced objects, I don’t know if there is such mechanism in Fluttet/Dart?

How I can destroy the scrollController object manually?

Does the scrollController object persiste even after its declaration scope (the build() method) ends?

Yes there is.
Dart VM has a garbage collector and you can even check memory usage. See Using the Memory view | Flutter

2 Likes

if this is the case, why I do need to destroy scrollController object manually? what I’m missing?

my reasoning is as follow:

  • the scrollController is created and attached a listener in build() method.
  • as the widget is stateless the build() method is only called once.
  • scrollController lifecycle is related to the widget.
  • when Flutter destroy the widget scrollController will not be referenced.
  • the GC will take care of scrollController and its listeners.
1 Like

You might want to think about whether you need to use a Stateful Widget vs a Stateless Widget for whatever you are building.

If you are using a statefulWidget, then keeping the scrollController in the state would make sense, and you should use the dispose() method to clean up any listeners of the scroll controller. This is recommended practice.

If you use a StatelessWidget you will lose the scroll controller every time the UI gets rebuilt. Stateful widgets keep the state across rebuilds, so it’s also more efficient.

Here’s an exercise for you :slight_smile:

Play with the memory view on devtools, maybe you can find out more information about how many scrollController instances you see!

2 Likes

I found this article about Dart Garbage Collector interesting: Flutter: Don’t Fear the Garbage Collector | by Matt Sullivan | Flutter | Medium

My takeaway (I may be wrong), is that Flutter/Dart GC is optimized for short lived objects, this is why Widgets are created and destroyed frequently without impacting the performance.
In the context of my question, I still prefer using a Stateless Widget to solve the problem of Scroll Controller listener, as it’s a very simple object and let the GC clean up the memory instead of using the arguably complex solution of Stateful Widget and override Lifecyle methods (initState() and destroy()).

One more optimization I did for the shared solution above, is that I moved the ScrollController instantiation out of the build() method, so it wont be created every time the widget get rebuilt.

Thanks for sharing, very helpful for me.

1 Like

i was reading the documentation and it seemed logical to just use a notification listener instead of a controller

if (snapshot.connectionState == ConnectionState.done) {
          // 5
          return NotificationListener<ScrollNotification>(
            onNotification: (ScrollNotification scrollNotification) {
              if (scrollNotification.metrics.atEdge) {
                print(scrollNotification.metrics.pixels ==
                        scrollNotification.metrics.minScrollExtent
                    ? 'On top'
                    : 'At the end');
              }
              return true;
            },
            child: ListView(
2 Likes

A widget build method might be called dozens of times a second. Perhaps this isn’t well understood… 60 fps?

Given this, it isn’t correct to needlessly construct and wire up complex objects (and then make them available for destruction) within that method. Incorporating a widget instead allows the Framework to cache and reuse that which doesn’t change between those (potentially many) method calls.

Imagine if your approach was used throughout a widget tree containing thousands of widgets?

1 Like

Many thanks for the replay @jbadda
Indeed, I wasn’t aware of this fact when I first asked the question, but thankfully I realized my mistake and understood how the build() method works, I changed my solution to something else (hopefully better),

I didn’t want to use a StatefulWidget because I don’t understand why we need to dispose() the controller manually, as it should be cleaned by the GC when the widget is disposed, in the code bellow _controller = MyScrollController(); will not be referenced after ExploreScreen is disposed.

Maybe I miss something, I really want an answer for that question :thinking:

here is the current solution code:

class MyScrollController extends ScrollController {
  MyScrollController() {
    addListener(createListener);
  }

  void createListener() {
    if (offset == position.minScrollExtent) {
      print('top reached!');
    }
    if (offset == position.maxScrollExtent) {
      print('bottom reached');
    }
  }
}

class ExploreScreen extends StatelessWidget {
  final mockService = MockFooderlichService();
  final _controller = MyScrollController();

  ExploreScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    //print('ExploreScreen build() / ${_controller}');
    //_controller = MyScrollController();
    return FutureBuilder(
      future: mockService.getExploreData(),
      builder: (context, AsyncSnapshot<ExploreData> snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          return ListView(
            scrollDirection: Axis.vertical,
            controller: _controller,
            children: [
              TodayRecipeListView(recipes: snapshot.data?.todayRecipes ?? []),
              const SizedBox(height: 16),
              FriendPostListView(
                friendPosts: snapshot.data?.friendPosts ?? [],
              )
            ],
          );
        } else {
          return const Center(child: CircularProgressIndicator());
        }
      },
    );
  }
}

Well perhaps consider that if you add no listeners to your controller (or create some other custom long lived object referenced to it), you need no dispose method offering you the opportunity to override the default and tidy up things you’ve used or created.

However you do. You add a listener. Where is that listener removed so that the underlying ChangeNotifier might have no listeners attached so it can be garbage collected?

In other word, how can your controller be garbage collected with a listener referenced to it? How about the widget that owns the controllers?

Hope that makes sense.

2 Likes

Thanks for the insights, I’m exploring and I hope all this will make sense to me soon :slight_smile:

I have just checked the code, and found that class ScrollController extends ChangeNotifier so basically A ScrollController object is a ChangeNotifier object, that have _listeners as member field defined as List<VoidCallback?> _listeners = List<VoidCallback?>.filled(0, null);
As I understand, the ChangeNotifier object (i.e the ScrollController) holds a reference to the list of listeners, and not the inverse, so the listeners will not prevent the object from being garbage collected? am I wrong? is there any thing special about ChangeNotifier objects? like being referenced from the framework?