visitChildrenForSemantics method
Filters out children whose nodes have been removed from the controller (or are mid-exit) so screen readers don't announce/focus them while the render boxes wait for their post-frame eviction.
Walks in visual order (sticky headers first, then in-flow visible nodes top-to-bottom) so screen readers announce rows in the same order the user sees them rather than raw insertion order.
Implementation
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
// Sticky headers paint on top, shallowest first (visual top).
for (final sticky in _sticky.headers) {
final child = getChildForNode(sticky.nodeId);
if (child == null) continue;
if (controller.getNodeData(sticky.nodeId) == null) continue;
if (controller.isExiting(sticky.nodeId)) continue;
visitor(child);
}
// Then in-flow visible nodes, skipping any already emitted as sticky.
// Iterate the MOUNTED set (bounded by the cache region plus retained
// rows) rather than the full visible order, which would pay an
// O(N_visible) sweep per semantics pass to visit O(mounted) children.
// Sort by visible index so screen readers still get top-to-bottom
// visual order.
debugLastSemanticsIterationCount = 0;
final inFlow = <(int, RenderBox)>[];
for (final entry in _children.entries) {
debugLastSemanticsIterationCount++;
final nodeId = entry.key;
final nid = _controller.nidOf(nodeId);
if (nid < 0) continue;
if (_sticky.isSticky(nid)) continue;
if (controller.getNodeData(nodeId) == null) continue;
if (controller.isExiting(nodeId)) continue;
final idx = _controller.visibleIndexOfNid(nid);
if (idx < 0) continue;
inFlow.add((idx, entry.value));
}
inFlow.sort((a, b) => a.$1.compareTo(b.$1));
for (final e in inFlow) {
visitor(e.$2);
}
}