skip to Main Content

I am developing using Flutter.
I encountered some unexpected behavior with widget rebuilds and ScrollControllers.

Below is the program that reproduces the issue.
In my understanding, I thought that widget rebuilds due to state changes are performed by the Flutter engine every frame. Therefore, when running the program below and scrolling the screen, I expected the process to proceed alternately as "scroll event -> rebuild -> scroll event -> rebuild -> …".
However, contrary to my expectation, scroll events may occur continuously. Moreover, this behavior often happens right after the first scroll after the screen is initialized. An example of the execution result with my comment is provided below.

Could anyone please explain the reason behind this?

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int counter = 0;
  final _controller = ScrollController();

  @override
  void initState() {
    super.initState();
    _controller.addListener(() {
      print("onScroll event");
      setState(() {
        counter = counter + 1;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    print("build: $counter");

    return Scaffold(
      body: ListView.builder(
        itemCount: 1000,
        controller: _controller,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('Item ${index + 1}'),
          );
        },
      ),
    );
  }
}
flutter: build: 0
flutter: onScroll event
flutter: build: 1
flutter: onScroll event
flutter: onScroll event // Why do those events run before rebuild?
flutter: onScroll event //
flutter: build: 4
flutter: onScroll event
flutter: build: 5
flutter: onScroll event
flutter: build: 6
flutter: onScroll event
flutter: build: 7
flutter: onScroll event
flutter: build: 8
flutter: onScroll event
flutter: build: 9
flutter: onScroll event
flutter: build: 10
flutter: onScroll event
flutter: build: 11
flutter: onScroll event
flutter: build: 12
flutter: onScroll event
flutter: build: 13
flutter: onScroll event
flutter: build: 14
flutter: onScroll event
...

Thanks 🙂

2

Answers


  1. Chosen as BEST ANSWER

    Inspired by Hamed's answer, I came up with the following code that rebuilds the widget immediately after a scroll event. This works as expected, but I'm creating variables outside of the widget, which I don't think is smart.

    import 'package:flutter/material.dart';
    import 'package:hooks_riverpod/hooks_riverpod.dart';
    
    void main() {
      runApp(const ProviderScope(child: MyApp()));
    }
    
    int _counter = 0;
    
    final scrollControllerProvider =
        ChangeNotifierProvider<ScrollController>((ref) {
      return ScrollController();
    });
    
    final scrollEventProvider = Provider<int>((ref) {
      ref.watch(scrollControllerProvider);
      print("watch scroll change");
      return _counter++;
    });
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return const MaterialApp(
          home: MyHomePage(),
        );
      }
    }
    
    class MyHomePage extends ConsumerStatefulWidget {
      const MyHomePage({super.key});
    
      @override
      ConsumerState<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends ConsumerState<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        final controller = ref.read(scrollControllerProvider);
        ref.watch(scrollEventProvider);
    
        print("build: $_counter");
    
        return Scaffold(
          body: ListView.builder(
            itemCount: 1000,
            controller: controller,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('Item ${index + 1}'),
              );
            },
          ),
        );
      }
    }
    
    
    flutter: build: 5
    flutter: watch scroll change
    flutter: build: 6
    flutter: watch scroll change
    flutter: build: 7
    flutter: watch scroll change
    flutter: build: 8
    flutter: watch scroll change
    flutter: build: 9
    flutter: watch scroll change
    flutter: build: 10
    flutter: watch scroll change
    flutter: build: 11
    flutter: watch scroll change
    flutter: build: 12
    flutter: watch scroll change
    flutter: build: 13
    flutter: watch scroll change
    flutter: build: 14
    flutter: watch scroll change
    ...
    

  2. Scrolling a ListView in Flutter continuously generates scroll events by the attached ScrollController. These events are processed asynchronously, and the widget rebuilds triggered by the setState method are scheduled for the next frame. As a result, you may observe multiple scroll events happening before the widget is actually rebuilt, leading to the unexpected behavior you’re experiencing. This is because the scroll events are dispatched immediately, while the rebuild process is scheduled later. Once the rebuild is completed, you will see the updated widget reflecting the changes.

    You can change your code like this:

    class _MyHomePageState extends State<MyHomePage> {
      int counter = 0;
      final _controller = ScrollController();
      bool _isScrolling = false;
    
      @override
      void initState() {
        super.initState();
        _controller.addListener(() {
          if (!_isScrolling) {
            setState(() {
              counter = counter + 1;
              _isScrolling = true;
            });
    
            WidgetsBinding.instance.addPostFrameCallback((_) {
              _isScrolling = false;
            });
          }
        });
      }
    
      ...
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search