Skip to content

[cocoon] Migrate LatticeScrollView to TableView from two_dimensional_scrollables - #5138

Draft
andywolff wants to merge 1 commit into
flutter:mainfrom
andywolff:cocoon-dashboard-scroll
Draft

[cocoon] Migrate LatticeScrollView to TableView from two_dimensional_scrollables#5138
andywolff wants to merge 1 commit into
flutter:mainfrom
andywolff:cocoon-dashboard-scroll

Conversation

@andywolff

@andywolff andywolff commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes flutter/flutter#191490

Replaces the custom RenderBox-based LatticeScrollView layout engine in
dashboard/lib/widgets/lattice.dart with TableView.builder from the official
two_dimensional_scrollables: ^0.3.0 package.

I configured pinnedRowCount: 1 and pinnedColumnCount: 1 to preserve sticky
header rows and author columns. To maintain high-frame-rate scrolling on Flutter
Web across large grids, I introduced custom lightweight render objects:
_LatticeCellBox (LeafRenderObjectWidget) for standard matrix cells (0 child
widgets, direct canvas painting) and _LatticeCellChildBox
(SingleChildRenderObjectWidget) for header/icon cells.

During testing, I observed two interaction bottlenecks:

  1. _processCommitStatuses was re-instantiating thousands of cell objects on
    every scroll frame. I added identity-based caching in _TaskGridState using
    identical(_lastCommitStatuses, widget.commitStatuses).
  2. Vertical scroll physics claimed Shift+wheel mouse events whenever vertical
    scroll position was less than maximum extent. I added
    _LatticeVerticalScrollPhysics to explicitly reject vertical offsets when
    Shift is held down (isShiftPressed == true), enabling instant horizontal
    Shift-scrolling.
Metric / Aspect Custom RenderBox (LatticeScrollView) Refactored TableView
Code Size ~1,000 LOC ~375 LOC
Maintenance Custom matrix layout engine Maintained by Flutter team
Data Cells Heavy widget wrappers Direct LeafRenderObjectWidget canvas painting
Cell Allocation Regenerated every frame Identity-cached (_getCells)
Shift+Scroll Custom listener & manual delta math Instant via _LatticeVerticalScrollPhysics

Additionally, I updated _handleTapUp to compute coordinates relative to pinned
header extents and current scroll offsets (scrollX/scrollY), and added support
for --dart-define=USE_PRODUCTION_SERVICE=false in dashboard/lib/main.dart
for local release mode testing.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read the Flutter Style Guide recently, and have followed its advice.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the LatticeScrollView to use TableView.builder from the two_dimensional_scrollables package, which greatly simplifies the layout, painting, and hit-testing logic. It also introduces caching for lattice cells in _TaskGridState to prevent redundant matrix regeneration and updates tests to accommodate the new caching behavior. The review feedback suggests simplifying the Shift key detection logic in lattice.dart by using HardwareKeyboard.instance.isShiftPressed directly, and refactoring the private helper _getCells in task_grid.dart to remove the redundant widget parameter.

Comment on lines +43 to +54
bool shouldAcceptUserOffset(ScrollMetrics position) {
final keys = HardwareKeyboard.instance.logicalKeysPressed;
final isShiftPressed =
keys.contains(LogicalKeyboardKey.shiftLeft) ||
keys.contains(LogicalKeyboardKey.shiftRight) ||
HardwareKeyboard.instance.isShiftPressed;

if (isShiftPressed) {
return false;
}
return super.shouldAcceptUserOffset(position);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The manual check of logicalKeysPressed for shiftLeft and shiftRight is redundant because HardwareKeyboard.instance.isShiftPressed already checks both of these keys internally. You can simplify this logic to just use HardwareKeyboard.instance.isShiftPressed directly.

  @override
  bool shouldAcceptUserOffset(ScrollMetrics position) {
    if (HardwareKeyboard.instance.isShiftPressed) {
      return false;
    }
    return super.shouldAcceptUserOffset(position);
  }

Comment on lines +82 to 87
bool get _isShiftPressed {
final keys = HardwareKeyboard.instance.logicalKeysPressed;
return keys.contains(LogicalKeyboardKey.shiftLeft) ||
keys.contains(LogicalKeyboardKey.shiftRight) ||
HardwareKeyboard.instance.isShiftPressed;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly, the _isShiftPressed getter can be simplified to directly return HardwareKeyboard.instance.isShiftPressed.

  bool get _isShiftPressed => HardwareKeyboard.instance.isShiftPressed;

Comment on lines +185 to 203
List<List<LatticeCell>> _getCells(TaskGrid widget) {
if (_cachedCells != null &&
identical(_lastCommitStatuses, widget.commitStatuses) &&
identical(_lastFilter, widget.filter)) {
return _cachedCells!;
}
_lastCommitStatuses = widget.commitStatuses;
_lastFilter = widget.filter;
_cachedCells = _processCommitStatuses(widget);
return _cachedCells!;
}

@override
Widget build(BuildContext context) {
return LatticeScrollView(
// TODO(ianh): Provide some vertical scroll physics that disable
// the clamping in the vertical direction, so that you can keep
// scrolling past the end instead of hitting a wall every time
// we load.
// TODO(ianh): Trigger the loading from the scroll offset,
// rather than the current hack of loading during build.
cells: _processCommitStatuses(widget),
cells: _getCells(widget),
verticalController: verticalController,
horizontalController: horizontalController,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _getCells method is a private helper inside _TaskGridState and does not need to take TaskGrid widget as a parameter, since State already provides direct access to the widget property. Removing this parameter simplifies the method signature and call site.

Suggested change
List<List<LatticeCell>> _getCells(TaskGrid widget) {
if (_cachedCells != null &&
identical(_lastCommitStatuses, widget.commitStatuses) &&
identical(_lastFilter, widget.filter)) {
return _cachedCells!;
}
_lastCommitStatuses = widget.commitStatuses;
_lastFilter = widget.filter;
_cachedCells = _processCommitStatuses(widget);
return _cachedCells!;
}
@override
Widget build(BuildContext context) {
return LatticeScrollView(
// TODO(ianh): Provide some vertical scroll physics that disable
// the clamping in the vertical direction, so that you can keep
// scrolling past the end instead of hitting a wall every time
// we load.
// TODO(ianh): Trigger the loading from the scroll offset,
// rather than the current hack of loading during build.
cells: _processCommitStatuses(widget),
cells: _getCells(widget),
verticalController: verticalController,
horizontalController: horizontalController,
);
List<List<LatticeCell>> _getCells() {
if (_cachedCells != null &&
identical(_lastCommitStatuses, widget.commitStatuses) &&
identical(_lastFilter, widget.filter)) {
return _cachedCells!;
}
_lastCommitStatuses = widget.commitStatuses;
_lastFilter = widget.filter;
_cachedCells = _processCommitStatuses(widget);
return _cachedCells!;
}
@override
Widget build(BuildContext context) {
return LatticeScrollView(
cells: _getCells(),
verticalController: verticalController,
horizontalController: horizontalController,
);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[cocoon] Migrate LatticeScrollView to TableView from package two_dimensional_scrollables

1 participant