Files
buzz/mobile/lib/features/channels/members_sheet/member_section.dart
Tom Brow bf4a163510 fix(mobile): lazily lay out channel member groups (#8437)
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.

![Synthetic member
groups](https://raw.githubusercontent.com/block/buzz/c327b1d973e6ec54a09358e8f6794dc0a2587304/pr-8437--members.png)

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
2026-10-20 15:14:29 -07:00

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,
),
),
],
),
);
}