Flutter scroll lags and shakes when list items have different heights in the list

Hi,
I’m facing an issue in my Flutter desktop app for Windows when scrolling through a list with items of different heights.

When I drag the scrollbar, it noticeably lags behind the mouse cursor and sometimes shakes or jitters.

Gif with shakes on scrolling

The similar thing happens when scrolling with the mouse wheel — the scroll is shaking.

Gif with shakes on scrolling by mouse wheel (I’m only scrolling up)

This only seems to happen when the items in the scroll view have varying heights. Has anyone dealt with this before?

Script with the problem:

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:infinite_scrolling_example_flutter/app/state.dart';

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

  @override
  State<InfiniteScrollList> createState() => _InfiniteScrollListState();
}

class _InfiniteScrollListState extends State<InfiniteScrollList> {
  final ScrollController _controller = ScrollController();

  @override
  void initState() {
    super.initState();
    _controller.addListener(scrollListener);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void scrollListener() {
    final appState = Provider.of<AppState>(context, listen: false);
    final maxVerticalScrollBound = _controller.position.maxScrollExtent;
    final triggerPoint = maxVerticalScrollBound * 0.7;
    final currentScrollOffset = _controller.offset;

    if (currentScrollOffset > 0.0 && !appState.showFab) {
      appState.showFabButton(true);
    }

    if (currentScrollOffset == 0.0 && appState.showFab) {
      appState.showFabButton(false);
    }

    if (currentScrollOffset > triggerPoint && !appState.isLoading) {
      appState.appendList();
    }
  }

  void scrollToTop() {
    _controller.animateTo(
      0.0,
      duration: const Duration(milliseconds: 300),
      curve: Curves.bounceInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      child: Consumer<AppState>(
        builder: (context, value, child) {
          return Scaffold(
            appBar: AppBar(
              automaticallyImplyLeading: false,
              title: const Text('Infinite Scrolling List'),
              centerTitle: true,
            ),
            floatingActionButton: value.showFab
                ? FloatingActionButton(
                    onPressed: scrollToTop,
                    child: const Icon(Icons.arrow_circle_up_rounded),
                  )
                : null,
            body: Scrollbar(
              controller: _controller,
              interactive: true,
              child: ListView.custom(
                controller: _controller,
                physics: const ClampingScrollPhysics(),
                childrenDelegate: SliverChildBuilderDelegate(
                  (context, index) {
                    if (index == value.intList.length) {
                      return const Padding(
                        padding: EdgeInsets.symmetric(vertical: 20),
                        child: Center(child: CircularProgressIndicator()),
                      );
                    }

                    final element = value.intList[index];
                    final randomSymbols = generateRandomSymbols();

                    return Padding(
                      padding: const EdgeInsets.symmetric(
                          vertical: 10, horizontal: 20),
                      child: Container(
                        decoration: BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.circular(12),
                          boxShadow: [
                            BoxShadow(
                              color: Colors.black.withOpacity(0.1),
                              blurRadius: 3.0,
                              spreadRadius: 3.0,
                            ),
                          ],
                        ),
                        child: ListTile(
                          title: Text("Element No $element $randomSymbols"),
                          subtitle: Text("Is Even => ${element.isEven}"),
                        ),
                      ),
                    );
                  },
                  childCount:
                      value.intList.length + (value.isLoading ? 1 : 0),
                  addAutomaticKeepAlives: false,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

/// Generates a string of random symbols
/// 
/// [count] - Number of symbols to generate (optional, defaults to random between 100-10000)
/// [includeLineBreaks] - Whether to add line breaks every 80 characters (default: false)
String generateRandomSymbols({int? count, bool includeLineBreaks = false}) {
  final random = Random();
  final symbolCount = count ?? (random.nextInt(9901) + 100);

  final symbols = [
    '!', '@', '#', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+',
    '[', ']', '{', '}', '|', '\\', ':', ';', '"', "'", '<', '>', ',',
    '.', '?', '/', '~', '`', '0', '1', '2', '3', '4', '5', '6', '7',
    '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K',
    'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
    'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
    'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
    'y', 'z', '☺', '☻', '♠', '♣', '♥', '♦', '♪', '♫', '☼', '►', '◄',
    '↕', '‼', '¶', '§', '▬', '↨', '↑', '↓', '→', '←', '∟', '↔', '▲', '▼'
  ];

  final buffer = StringBuffer();
  for (int i = 0; i < symbolCount; i++) {
    buffer.write(symbols[random.nextInt(symbols.length)]);
    if (includeLineBreaks && (i + 1) % 80 == 0) {
      buffer.write('\n');
    }
  }

  return buffer.toString();
}

Full source code > (The problematic script)

Any help would be appreciated!
Thank you!

You change the Provider state on EVERY scroll and wrap your entire page on listening that very state and come here to say “FLUTTER LAGS”???

Are you kidding, right?

Wouldn’t it be better to explain than to sarcastically shame?

2 Likes

It would, if he didn’t blame the tool for it.

If someone doesn’t know better I prefer to be kind and teach

4 Likes

Then teach!

This is right though, it’s even in the provider readme to not put too many widgets in the consumer. Especially if it updates often. The lag is definitely because of that.

If you want infinite scrolling there are packages for that, I don’t think provider is the right tool for that.

But yeah I have noticed this shakes too on mobiles while scrolling ListView really slow. In at least 3 different apps, also saw some bug report about it. I think it’s worse if your widgets are different height and you can’t use itemExtent because of this. This whole itemExtent situation feels like a workaround for listview’s performance. There are some replacement packages for ListView that are supposedly better and more performant… Never got around to trying them.

Actually you can achieve really fast and smooth scrolling with an ListView.builder. You can do the whole scrolling logic on your data layer by pulling new data everytime your litembuilder requests a item at the end of your already loaded list you load the next page and update the itemCount of your List View.builder.
Your ListView won’t jump but continue to scroll smoothly.
That’s how we do it in our app everywhere. And if you rebuild your ListView.builder with an updated itemCount using Provider, watch_it or an ListenableBuilder doesn’t matter

Take care to avoid a situation where you have nested scroll views. The performance gains from ListView.builder come from only building the elements that are estimated to be in view. If you nest list views constructing that estimate involves building those other lists, which defeats the purpose. Even if your widget hierarchy represents a nested data structure, you will want to transform it into a flat list for the view for performance reasons.

Or you need to use one of the multi_sliver packages. We have a lot of nested scrollviews it all depends how you do it.

Try Superlist