mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Opening a large channel member sheet mounted and laid out every person in a grouped column. A captured Debug simulator entrance spent 242 ms in the UI callback, including 223 ms in layout. Build each person and agent as a lazy sliver row while preserving the grouped cards, names, statuses, management menus, and activity actions. Screen-reader row indices continue across both groups. A separate formatting-only commit fixes an existing carousel-test formatter discrepancy. No duplicate member-sheet performance PR or issue found. ### Validation - Added a 200-member widget regression: the original mounts 100 people initially; the fix mounts fewer than 20 nearby rows and reaches the final person and agent. - Added grouping, compact sizing, empty state, member tap identity, accessible indices, and scrolled member/agent menu checks. Existing channel-details “See all” navigation test passes. - Mobile formatting, static analysis, and the full mobile test suite passed on the committed tree. - Synthetic before/after rendering is pixel-identical at the same viewport. - Populated native iOS 26.5 simulator workflow: open members, scroll through people and agents to the end, then close. Final installed build passed this workflow. Debug, accessibility automation, profiler overhead, different Flutter versions, and shared-host load limit timing conclusions. This removes eager offscreen layout; it does not establish a controlled end-to-end speedup or release-device performance. ### Visual check Synthetic member sheet; before and after are pixel-identical at this viewport.  --------- Signed-off-by: Tom Brow <tomb@block.xyz>
76 lines
2.5 KiB
Dart
76 lines
2.5 KiB
Dart
part of '../members_sheet.dart';
|
|
|
|
/// The member card is split into independently laid-out rows so a large group
|
|
/// cannot force every offscreen member to mount during the sheet entrance.
|
|
class _MemberSection extends StatelessWidget {
|
|
const _MemberSection({
|
|
required this.label,
|
|
required this.itemCount,
|
|
required this.itemBuilder,
|
|
this.semanticIndexOffset = 0,
|
|
});
|
|
|
|
final String label;
|
|
final int itemCount;
|
|
final IndexedWidgetBuilder itemBuilder;
|
|
final int semanticIndexOffset;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => SliverPadding(
|
|
padding: const EdgeInsets.symmetric(vertical: Grid.xxs),
|
|
sliver: SliverMainAxisGroup(
|
|
slivers: [
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: Grid.half, bottom: Grid.xxs),
|
|
child: Text(
|
|
label,
|
|
style: context.textTheme.labelMedium?.copyWith(
|
|
color: context.colors.onSurfaceVariant,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, index) => Material(
|
|
color: context.colors.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: index == 0
|
|
? const Radius.circular(Radii.container)
|
|
: Radius.zero,
|
|
bottom: index == itemCount - 1
|
|
? const Radius.circular(Radii.container)
|
|
: Radius.zero,
|
|
),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: ListTileTheme.merge(
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: Grid.xs),
|
|
minLeadingWidth: 22,
|
|
horizontalTitleGap: Grid.xs,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (index > 0)
|
|
Divider(
|
|
height: 1,
|
|
thickness: 1,
|
|
indent: Grid.xs + 40 + Grid.xs,
|
|
endIndent: Grid.xs,
|
|
color: context.colors.onSurface.withValues(alpha: 0.07),
|
|
),
|
|
itemBuilder(context, index),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
childCount: itemCount,
|
|
semanticIndexOffset: semanticIndexOffset,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|