Created
September 16, 2025 18:19
-
-
Save ditman/0f766818f6a21b9070d94419eb7c726d to your computer and use it in GitHub Desktop.
Virtual Rows across widgets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math'; | |
import 'package:flutter/material.dart'; | |
// import 'package:flutter_lorem/flutter_lorem.dart'; | |
import 'package:provider/provider.dart'; | |
/// Keeps track of the max sizes of each virtual row. | |
class VirtualTableRowSizes extends ChangeNotifier { | |
final Map<Key, double> _maxRowHeights = <Key, double>{}; | |
/// Updates the maximum height for a given [rowKey]. | |
/// | |
/// If [newHeight] is greater than the currently stored maximum height for [rowKey], | |
/// or if [rowId] is new, the height is updated, and listeners are notified. | |
void updateRowHeight(Key rowKey, double newHeight) { | |
if (!_maxRowHeights.containsKey(rowKey) || | |
newHeight > (_maxRowHeights[rowKey] ?? 0.0)) { | |
_maxRowHeights[rowKey] = newHeight; | |
notifyListeners(); | |
} | |
} | |
/// Retrieves the maximum observed height for a given [rowKey]. | |
double? getRowHeight(Key rowKey) { | |
return _maxRowHeights[rowKey]; | |
} | |
} | |
/// Ensures all [child]ren with the same [rowKey] maintain the height of the | |
/// tallest [child]. | |
/// | |
/// Uses the [VirtualTableRowSizes] provider. | |
class VirtualTableRow extends StatefulWidget { | |
final Widget child; | |
final Key rowKey; | |
const VirtualTableRow({required this.child, required this.rowKey, super.key}); | |
@override | |
State<VirtualTableRow> createState() => _VirtualTableRowState(); | |
} | |
class _VirtualTableRowState extends State<VirtualTableRow> { | |
// Schedules and reports a measurement cycle. This will cause the VirtualTableRowSizer | |
// to propagate a height update for the rowKey if needed | |
void _scheduleMeasurement(BuildContext context) { | |
WidgetsBinding.instance.addPostFrameCallback((Duration _) { | |
final RenderBox? renderBox = context.findRenderObject() as RenderBox?; | |
if (renderBox != null) { | |
Provider.of<VirtualTableRowSizes>( | |
context, | |
listen: false, | |
).updateRowHeight(widget.rowKey, renderBox.size.height); | |
} | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
_scheduleMeasurement(context); | |
return Consumer<VirtualTableRowSizes>( | |
builder: (context, value, child) { | |
final rowHeight = value.getRowHeight(widget.rowKey); | |
return ConstrainedBox( | |
constraints: BoxConstraints(minHeight: rowHeight ?? 0), | |
child: widget.child, | |
); | |
}, | |
); | |
} | |
} | |
/// Renders Lorem Ipsum text inside a [VirtualTableRow], with a `rowId` | |
/// specified from the outside. When clicked, the Lorem Ipsum gets a | |
/// little longer. | |
class RowItem extends StatefulWidget { | |
final int rowId; | |
const RowItem({super.key, required this.rowId}); | |
@override | |
State<RowItem> createState() => _RowItemState(); | |
} | |
class _RowItemState extends State<RowItem> { | |
int _length = _random.nextInt(10) + 1; | |
String _lipsum = ''; | |
static final _random = Random(); | |
@override | |
void initState() { | |
super.initState(); | |
_lipsum = lorem(words: _length); | |
} | |
// Updates _lipsum with a few more words. | |
void _updateLipsum() { | |
setState(() { | |
_length += _random.nextInt(10); | |
_lipsum = lorem(words: _length); | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return VirtualTableRow( | |
rowKey: ValueKey<int>(widget.rowId), | |
child: InkWell( | |
onTap: _updateLipsum, | |
child: Container( | |
color: Colors.blue.withValues(alpha: 0.1), | |
alignment: Alignment.center, | |
child: Text(_lipsum, style: const TextStyle(color: Colors.blueGrey)), | |
), | |
), | |
); | |
} | |
} | |
/// Renders a Card with a bunch of rows inside it. | |
class CarouselCardWithRows extends StatelessWidget { | |
const CarouselCardWithRows({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Card( | |
child: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Column( | |
spacing: 8.0, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[for (int i = 0; i < 3; i++) RowItem(rowId: i)], | |
), | |
), | |
); | |
} | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return ChangeNotifierProvider<VirtualTableRowSizes>( | |
// Create an instance of VirtualTableRowSizer and provide it to the widget tree. | |
create: (BuildContext context) => VirtualTableRowSizes(), | |
builder: (BuildContext context, Widget? child) { | |
return MaterialApp( | |
title: 'Virtual Table Rows', | |
theme: ThemeData(primarySwatch: Colors.blue), | |
home: const HomePage(), | |
); | |
}, | |
); | |
} | |
} | |
/// The home page of the application, displaying a list of virtual rows. | |
class HomePage extends StatelessWidget { | |
const HomePage({super.key}); | |
final _itemExtent = 250.0; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(title: const Text('Virtual Table Rows')), | |
body: CarouselView( | |
itemExtent: _itemExtent, | |
shrinkExtent: _itemExtent, | |
enableSplash: false, | |
children: [for (int i = 0; i < 4; i++) CarouselCardWithRows()], | |
), | |
); | |
} | |
} | |
void main() { | |
runApp(const MyApp()); | |
} | |
String lorem({int words = 5}) { | |
if (words == 0) return ""; | |
final List<String> lipsum = | |
''' | |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor | |
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis | |
nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. | |
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu | |
fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, | |
sunt in culpa qui officia deserunt mollit anim id est laborum. | |
''' | |
.replaceAll(RegExp(r'\W'), ' ') | |
.replaceAll(RegExp(r'\s+'), ' ') | |
.trim() | |
.split(' ') | |
.toSet() | |
.toList() | |
..shuffle(); | |
String soup = '${lipsum.take(words).join(' ').toLowerCase()}.'; | |
return soup[0].toUpperCase() + soup.substring(1); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment