I have a ListView of widgets that depend on asynchronous Riverpod providers for their content. These widgets show a CircularProgressIndicator while the AsyncValue is loading, and renders the full widget when it has a value. I don’t know the final height of the widget ahead of time.
The ListView uses a builder, so when items are scrolled off screen, they are unloaded, and rebuilt when scrolled back into view. This triggers the widget to go through the process of waiting on the AsyncValue again, showing the CircularProgressIndicator, which temporarily makes the widget shorter. When this happens, the ListView “jerks”.
In order to prevent this, I have created a widget that pays attention to its own size, caches the tallest that it’s ever been, and wraps its child in a Container with a minHeight constraint. This seems to work very well!
There are some potential gotchas:
- Doesn’t work if the
CircularProgressIndicatoris taller than the widget’s actual contents (not a problem for my use case). - The height cache value needs to be evicted if the widget contents change in a way that would make it smaller (pretty straightforward but I haven’t done this yet).
- The widget needs a
GlobalKeyto work, and I have to make sure I keep using the sameGlobalKeyafter widgets are rebuilt (also pretty straightforward).
I’m an experienced developer but relatively new to Flutter and Dart. Are there any other reasons why this might be a bad idea that I don’t know about? Or any different approaches that might be more idiomatically correct?
Thanks for looking! Here’s the code for the widget:
import 'package:flutter/material.dart';
/// A widget that ensures its child will always be at least as tall as it
/// ever was.
class NeverShrinkWidget extends StatelessWidget {
const NeverShrinkWidget({super.key, required this.child});
final Widget child;
static final Map<GlobalKey, double> _maxHeights = {};
void _recordMaxHeight() {
if (key is! GlobalKey) return;
final globalKey = key as GlobalKey;
final keyContext = globalKey.currentContext;
if (keyContext == null || !keyContext.mounted) return;
final box = keyContext.findRenderObject() as RenderBox;
if (!_maxHeights.containsKey(globalKey) ||
_maxHeights[globalKey]! < box.size.height) {
_maxHeights[globalKey] = box.size.height;
}
}
double? _getMaxHeight() {
if (key is! GlobalKey) return null;
final globalKey = key as GlobalKey;
return _maxHeights[globalKey];
}
@override
Widget build(BuildContext context) {
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
WidgetsBinding.instance.addPostFrameCallback((_) => _recordMaxHeight());
return false;
},
child: Container(
constraints: BoxConstraints(minHeight: _getMaxHeight() ?? 0),
child: SizeChangedLayoutNotifier(
child: child,
),
),
);
}
}