Skip to content

Commit 3747ce3

Browse files
richardthe3rdclaude
andcommitted
perf(drink-card): replace IntrinsicHeight with BoxDecoration left border for accent bar
IntrinsicHeight forces an extra layout pass on every card in a scrolling list. Switching to a 4px left BorderSide via BoxDecoration achieves the same visual result with no additional layout cost. Also updates the accent bar tests to assert on the BoxDecoration border rather than Container.color. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c961958 commit 3747ce3

2 files changed

Lines changed: 89 additions & 86 deletions

File tree

lib/widgets/drink_card.dart

Lines changed: 68 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -58,89 +58,84 @@ class DrinkCard extends StatelessWidget {
5858
child: InkWell(
5959
onTap: onTap,
6060
borderRadius: BorderRadius.circular(12),
61-
child: IntrinsicHeight(
62-
child: Row(
63-
crossAxisAlignment: CrossAxisAlignment.stretch,
64-
children: [
65-
Container(width: 4, color: accent),
66-
Expanded(
67-
child: Padding(
68-
padding: const EdgeInsets.all(16),
69-
child: Column(
70-
crossAxisAlignment: CrossAxisAlignment.start,
71-
children: [
72-
Row(
61+
child: Container(
62+
decoration: BoxDecoration(
63+
border: Border(left: BorderSide(color: accent, width: 4)),
64+
),
65+
child: Padding(
66+
padding: const EdgeInsets.all(16),
67+
child: Column(
68+
crossAxisAlignment: CrossAxisAlignment.start,
69+
children: [
70+
Row(
71+
crossAxisAlignment: CrossAxisAlignment.start,
72+
children: [
73+
Expanded(
74+
child: Column(
7375
crossAxisAlignment: CrossAxisAlignment.start,
7476
children: [
75-
Expanded(
76-
child: Column(
77-
crossAxisAlignment: CrossAxisAlignment.start,
78-
children: [
79-
SelectableText(
80-
drink.name,
81-
style: theme.textTheme.titleMedium?.copyWith(
82-
fontWeight: FontWeight.bold,
83-
),
84-
),
85-
const SizedBox(height: 4),
86-
SelectableText(
87-
drink.breweryLocation.isNotEmpty
88-
? '${drink.breweryName} • ${drink.breweryLocation}'
89-
: drink.breweryName,
90-
style: theme.textTheme.bodyMedium?.copyWith(
91-
color: colorScheme.onSurfaceVariant,
92-
),
93-
),
94-
],
77+
SelectableText(
78+
drink.name,
79+
style: theme.textTheme.titleMedium?.copyWith(
80+
fontWeight: FontWeight.bold,
9581
),
9682
),
97-
Semantics(
98-
label: drink.isFavorite ? 'Remove from favorites' : 'Add to favorites',
99-
hint: 'Double tap to toggle',
100-
button: true,
101-
child: IconButton(
102-
icon: Icon(
103-
drink.isFavorite ? Icons.favorite : Icons.favorite_border,
104-
color: drink.isFavorite
105-
? colorScheme.primary
106-
: colorScheme.onSurfaceVariant,
107-
),
108-
onPressed: onFavoriteTap,
83+
const SizedBox(height: 4),
84+
SelectableText(
85+
drink.breweryLocation.isNotEmpty
86+
? '${drink.breweryName} • ${drink.breweryLocation}'
87+
: drink.breweryName,
88+
style: theme.textTheme.bodyMedium?.copyWith(
89+
color: colorScheme.onSurfaceVariant,
10990
),
11091
),
11192
],
11293
),
113-
const SizedBox(height: 8),
114-
Wrap(
115-
spacing: 8,
116-
runSpacing: 4,
117-
children: [
118-
_CategoryChip(category: drink.category),
119-
if (drink.style != null)
120-
_StyleChip(style: drink.style!),
121-
ExcludeSemantics(
122-
child: InfoChip(
123-
label: '${drink.abv.toStringAsFixed(1)}%',
124-
icon: Icons.percent,
125-
),
126-
),
127-
ExcludeSemantics(
128-
child: InfoChip(
129-
label: StringFormattingHelper.capitalizeFirst(drink.dispense),
130-
icon: Icons.liquor,
131-
),
132-
),
133-
if (drink.availabilityStatus != null)
134-
_AvailabilityChip(status: drink.availabilityStatus!),
135-
if (drink.rating != null)
136-
_RatingChip(rating: drink.rating!),
137-
],
94+
),
95+
Semantics(
96+
label: drink.isFavorite ? 'Remove from favorites' : 'Add to favorites',
97+
hint: 'Double tap to toggle',
98+
button: true,
99+
child: IconButton(
100+
icon: Icon(
101+
drink.isFavorite ? Icons.favorite : Icons.favorite_border,
102+
color: drink.isFavorite
103+
? colorScheme.primary
104+
: colorScheme.onSurfaceVariant,
105+
),
106+
onPressed: onFavoriteTap,
107+
),
108+
),
109+
],
110+
),
111+
const SizedBox(height: 8),
112+
Wrap(
113+
spacing: 8,
114+
runSpacing: 4,
115+
children: [
116+
_CategoryChip(category: drink.category),
117+
if (drink.style != null)
118+
_StyleChip(style: drink.style!),
119+
ExcludeSemantics(
120+
child: InfoChip(
121+
label: '${drink.abv.toStringAsFixed(1)}%',
122+
icon: Icons.percent,
123+
),
124+
),
125+
ExcludeSemantics(
126+
child: InfoChip(
127+
label: StringFormattingHelper.capitalizeFirst(drink.dispense),
128+
icon: Icons.liquor,
138129
),
139-
],
140-
),
130+
),
131+
if (drink.availabilityStatus != null)
132+
_AvailabilityChip(status: drink.availabilityStatus!),
133+
if (drink.rating != null)
134+
_RatingChip(rating: drink.rating!),
135+
],
141136
),
142-
),
143-
],
137+
],
138+
),
144139
),
145140
),
146141
),

test/drink_card_test.dart

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -279,50 +279,58 @@ void main() {
279279
return Drink(product: product, producer: testProducer, festivalId: 'cbf2025');
280280
}
281281

282-
List<Color> accentColors(WidgetTester tester) =>
283-
tester.widgetList<Container>(find.byType(Container))
284-
.where((c) => c.color != null)
285-
.map((c) => c.color!)
286-
.toList();
282+
Color? accentBorderColor(WidgetTester tester) {
283+
for (final c in tester.widgetList<Container>(find.byType(Container))) {
284+
final decoration = c.decoration;
285+
if (decoration is BoxDecoration) {
286+
final border = decoration.border;
287+
if (border is Border) {
288+
final left = border.left;
289+
if (left.width == 4) return left.color;
290+
}
291+
}
292+
}
293+
return null;
294+
}
287295

288296
testWidgets('cider uses green accent', (WidgetTester tester) async {
289297
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('cider')));
290-
expect(accentColors(tester), contains(const Color(0xFF22C55E)));
298+
expect(accentBorderColor(tester), equals(const Color(0xFF22C55E)));
291299
});
292300

293301
testWidgets('perry uses lime accent', (WidgetTester tester) async {
294302
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('perry')));
295-
expect(accentColors(tester), contains(const Color(0xFF84CC16)));
303+
expect(accentBorderColor(tester), equals(const Color(0xFF84CC16)));
296304
});
297305

298306
testWidgets('mead uses gold accent', (WidgetTester tester) async {
299307
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('mead')));
300-
expect(accentColors(tester), contains(const Color(0xFFD97706)));
308+
expect(accentBorderColor(tester), equals(const Color(0xFFD97706)));
301309
});
302310

303311
testWidgets('wine uses purple accent', (WidgetTester tester) async {
304312
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('wine')));
305-
expect(accentColors(tester), contains(const Color(0xFF9333EA)));
313+
expect(accentBorderColor(tester), equals(const Color(0xFF9333EA)));
306314
});
307315

308316
testWidgets('international-beer uses red accent', (WidgetTester tester) async {
309317
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('international-beer')));
310-
expect(accentColors(tester), contains(const Color(0xFFEF4444)));
318+
expect(accentBorderColor(tester), equals(const Color(0xFFEF4444)));
311319
});
312320

313321
testWidgets('low-no uses cyan accent', (WidgetTester tester) async {
314322
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('low-no')));
315-
expect(accentColors(tester), contains(const Color(0xFF06B6D4)));
323+
expect(accentBorderColor(tester), equals(const Color(0xFF06B6D4)));
316324
});
317325

318326
testWidgets('apple-juice uses apple-green accent', (WidgetTester tester) async {
319327
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('apple-juice')));
320-
expect(accentColors(tester), contains(const Color(0xFF65A30D)));
328+
expect(accentBorderColor(tester), equals(const Color(0xFF65A30D)));
321329
});
322330

323331
testWidgets('unknown category uses navy fallback accent', (WidgetTester tester) async {
324332
await tester.pumpWidget(createTestWidget(drink: drinkWithCategory('unknown-type')));
325-
expect(accentColors(tester), contains(const Color(0xFF2B3170)));
333+
expect(accentBorderColor(tester), equals(const Color(0xFF2B3170)));
326334
});
327335
});
328336
}

0 commit comments

Comments
 (0)