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
CircularProgressIndicator
is 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
GlobalKey
to work, and I have to make sure I keep using the sameGlobalKey
after 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,
),
),
);
}
}