Skip to content

Instantly share code, notes, and snippets.

@kekland
Last active July 22, 2023 09:18
Show Gist options
  • Save kekland/7d0e1257957a3bf84b4f033377635eb0 to your computer and use it in GitHub Desktop.
Save kekland/7d0e1257957a3bf84b4f033377635eb0 to your computer and use it in GitHub Desktop.
Precomputed layout extent for items in a long ListView
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<String>? _items;
bool _isLoading = false;
double _horizontalExtent = 0;
List<double>? _cachedItemExtents;
ScrollController? _scrollController;
@override
void initState() {
super.initState();
}
TextStyle get _textStyle => Theme.of(context).textTheme.bodyMedium!;
Future<void> _loadItems() async {
setState(() {
_isLoading = true;
});
await Future.delayed(const Duration(seconds: 2));
final random = Random();
_items = List.generate(500, (index) {
final lines = random.nextInt(10) + 1;
// ignore: prefer_interpolation_to_compose_strings
return 'Item ${index + 1} - ' + 'Line\n' * lines + 'End';
});
_cacheItemExtents();
_isLoading = false;
setState(() {});
}
void _cacheItemExtents() {
final stopwatch = Stopwatch();
stopwatch.start();
final textPainter = TextPainter(
textDirection: TextDirection.ltr,
);
_cachedItemExtents = [];
for (final item in _items!) {
textPainter.text = TextSpan(
text: item,
style: _textStyle,
);
textPainter.layout(maxWidth: _horizontalExtent);
_cachedItemExtents!.add(textPainter.height);
}
_scrollController = PrecomputedScrollController(
itemExtents: _cachedItemExtents!,
);
textPainter.dispose();
// _scrollController = ScrollController();
stopwatch.stop();
debugPrint('Caching took ${stopwatch.elapsedMilliseconds}ms');
}
@override
Widget build(BuildContext context) {
late final Widget body;
if (_isLoading) {
body = const Center(
child: CircularProgressIndicator(),
);
} else if (_items == null) {
body = Center(
child: ElevatedButton(
onPressed: _loadItems,
child: const Text('Load items'),
),
);
} else {
body = ListView.builder(
controller: _scrollController,
itemCount: _items!.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
_items![index],
style: _textStyle,
),
);
},
);
}
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: LayoutBuilder(
builder: (context, constraints) {
_horizontalExtent = constraints.maxWidth;
return body;
},
),
);
}
}
class PrecomputedScrollController extends ScrollController {
PrecomputedScrollController({
required this.itemExtents,
});
final List<double> itemExtents;
@override
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition? oldPosition,
) {
return PrecomputedScrollPosition(
itemExtents: itemExtents,
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
}
class PrecomputedScrollPosition extends ScrollPositionWithSingleContext {
PrecomputedScrollPosition({
required this.itemExtents,
required super.physics,
required super.context,
super.debugLabel,
super.initialPixels,
super.keepScrollOffset,
super.oldPosition,
});
final List<double> itemExtents;
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
final maxScrollExtent = itemExtents.reduce((a, b) => a + b);
return super.applyContentDimensions(
minScrollExtent,
max(0, maxScrollExtent - viewportDimension),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment