perf: fix image loading performance across all screens

- Replace Image.network (no cache) with CachedNetworkImage in contributor_profile_screen
- Replace NetworkImage (no cache) with CachedNetworkImageProvider in desktop_topbar and contribute_screen (leaderboard avatars)
- Add maxWidthDiskCache + maxHeightDiskCache to all 23 CachedNetworkImage calls
- Add missing memCacheWidth/Height to review_card (36x36 avatar) and learn_more related events (140x100)
- Add dynamic memCache sizing to tier_avatar_ring based on widget size

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 20:03:03 +05:30
parent 5e00e431e3
commit 754b04dc05
10 changed files with 90 additions and 11 deletions

View File

@@ -125,6 +125,10 @@ class _ReviewCardState extends State<ReviewCard> {
imageUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=${Uri.encodeComponent(_review.username)}',
width: 36,
height: 36,
memCacheWidth: 72,
memCacheHeight: 72,
maxWidthDiskCache: 144,
maxHeightDiskCache: 144,
placeholder: (_, __) => CircleAvatar(
radius: 18,
backgroundColor: _avatarColor(_review.username),

View File

@@ -519,6 +519,8 @@ class _CalendarScreenState extends State<CalendarScreen> {
imageUrl: imgUrl,
memCacheWidth: 400,
memCacheHeight: 300,
maxWidthDiskCache: 800,
maxHeightDiskCache: 600,
height: 150,
width: double.infinity,
fit: BoxFit.cover,
@@ -582,6 +584,8 @@ class _CalendarScreenState extends State<CalendarScreen> {
imageUrl: imgUrl,
memCacheWidth: 300,
memCacheHeight: 300,
maxWidthDiskCache: 600,
maxHeightDiskCache: 600,
width: 100,
height: 100,
fit: BoxFit.cover,

View File

@@ -3,6 +3,7 @@
// 3 tabs: My Events · Submit Event · Reward Shop
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -11,6 +12,7 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import '../core/auth/auth_guard.dart';
import '../core/utils/error_utils.dart';
import '../features/gamification/models/gamification_models.dart';
import '../features/gamification/providers/gamification_provider.dart';
@@ -104,6 +106,9 @@ class _ContributeScreenState extends State<ContributeScreen>
super.initState();
PostHogService.instance.screen('Contribute');
WidgetsBinding.instance.addPostFrameCallback((_) {
// Gamification endpoints are authed — guests would hit 401 and pollute logs.
// AuthGuard.requireLogin prompts guests when they tap any gated action.
if (AuthGuard.isGuest) return;
final p = context.read<GamificationProvider>();
p.loadAll();
p.loadLeaderboard(); // independent — always fires regardless of loadAll TTL
@@ -389,7 +394,7 @@ class _ContributeScreenState extends State<ContributeScreen>
CircleAvatar(
radius: 20,
backgroundColor: _lightBlueBg,
backgroundImage: entry.avatarUrl != null ? NetworkImage(entry.avatarUrl!) : null,
backgroundImage: entry.avatarUrl != null ? CachedNetworkImageProvider(entry.avatarUrl!, maxWidth: 80, maxHeight: 80) : null,
child: entry.avatarUrl == null
? const Icon(Icons.person_outline, color: _blue, size: 20)
: null,
@@ -1334,12 +1339,14 @@ class _ContributeScreenState extends State<ContributeScreen>
}
Widget _textField(TextEditingController ctl, String placeholder, {
Key? key,
int maxLines = 1,
String? Function(String?)? validator,
TextInputType? keyboardType,
List<TextInputFormatter>? inputFormatters,
}) {
return TextFormField(
key: key,
controller: ctl,
maxLines: maxLines,
keyboardType: keyboardType,
@@ -1473,12 +1480,14 @@ class _ContributeScreenState extends State<ContributeScreen>
children: [
Expanded(
child: _textField(_latCtl, 'Latitude (e.g. 9.93123)',
key: const ValueKey('coord_lat'),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.\-]'))]),
),
const SizedBox(width: 12),
Expanded(
child: _textField(_lngCtl, 'Longitude (e.g. 76.26730)',
key: const ValueKey('coord_lng'),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[0-9.\-]'))]),
),
@@ -1488,7 +1497,9 @@ class _ContributeScreenState extends State<ContributeScreen>
Row(
children: [
Expanded(
child: _textField(_mapsLinkCtl, 'Paste Google Maps URL here'),
child: _textField(_mapsLinkCtl, 'Paste Google Maps URL here',
key: const ValueKey('coord_maps_url'),
keyboardType: TextInputType.url),
),
const SizedBox(width: 8),
SizedBox(

View File

@@ -2,6 +2,7 @@
// CTR-004 — Public contributor profile page.
// Shows avatar, tier ring, EP stats, and submission grid for any contributor.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../features/gamification/models/gamification_models.dart';
@@ -215,10 +216,15 @@ class _ContributorProfileScreenState extends State<ContributorProfileScreen> {
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: SizedBox.expand(
child: Image.network(
firstImage,
child: CachedNetworkImage(
imageUrl: firstImage,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(color: const Color(0xFF334155)),
memCacheWidth: 400,
memCacheHeight: 300,
maxWidthDiskCache: 800,
maxHeightDiskCache: 600,
placeholder: (_, __) => Container(color: const Color(0xFF1E293B)),
errorWidget: (_, __, ___) => Container(color: const Color(0xFF334155)),
),
),
),

View File

@@ -320,6 +320,8 @@ class _HomeContentState extends State<_HomeContent>
height: double.infinity,
memCacheWidth: 1400,
memCacheHeight: 800,
maxWidthDiskCache: 1400,
maxHeightDiskCache: 800,
placeholder: (_, __) => Container(
color: const Color(0xFF0A0E1A),
),
@@ -529,6 +531,8 @@ class _HomeContentState extends State<_HomeContent>
fit: BoxFit.cover,
memCacheWidth: 1400,
memCacheHeight: 800,
maxWidthDiskCache: 1400,
maxHeightDiskCache: 800,
)
else
Container(color: const Color(0xFF0A0E1A)),
@@ -782,6 +786,8 @@ class _HomeContentState extends State<_HomeContent>
imageUrl: img,
memCacheWidth: 600,
memCacheHeight: 320,
maxWidthDiskCache: 1200,
maxHeightDiskCache: 640,
width: double.infinity,
height: imageHeight,
fit: BoxFit.cover,

View File

@@ -2,6 +2,7 @@
import 'dart:async';
import 'dart:ui';
import '../core/utils/error_utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../core/auth/auth_guard.dart';
@@ -136,15 +137,16 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
_loading = false;
});
}
} catch (e) {
} catch (e, st) {
if (kDebugMode) debugPrint('HomeScreen._loadUserDataAndEvents error: $e\n$st');
if (mounted) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
}
}
// Refresh notification badge count (fire-and-forget)
if (mounted) {
// Refresh notification badge count (fire-and-forget, skip for guests — endpoint is authed)
if (mounted && !AuthGuard.isGuest) {
context.read<NotificationProvider>().refreshUnreadCount();
}
}
@@ -263,6 +265,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: imageUrl,
memCacheWidth: 112,
memCacheHeight: 112,
maxWidthDiskCache: 224,
maxHeightDiskCache: 224,
fit: BoxFit.contain,
placeholder: (_, __) => Icon(
icon ?? Icons.category,
@@ -475,6 +479,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: img,
memCacheWidth: 112,
memCacheHeight: 112,
maxWidthDiskCache: 224,
maxHeightDiskCache: 224,
width: 56,
height: 56,
fit: BoxFit.cover,
@@ -956,6 +962,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: imageUrl,
memCacheWidth: 160,
memCacheHeight: 160,
maxWidthDiskCache: 320,
maxHeightDiskCache: 320,
width: 80,
height: 80,
fit: BoxFit.cover,
@@ -1288,6 +1296,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: currentImg,
memCacheWidth: 200,
memCacheHeight: 200,
maxWidthDiskCache: 400,
maxHeightDiskCache: 400,
fit: BoxFit.cover,
placeholder: (_, __) => const SizedBox.shrink(),
errorWidget: (_, __, ___) => const SizedBox.shrink(),
@@ -1524,6 +1534,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: img,
memCacheWidth: 700,
memCacheHeight: 400,
maxWidthDiskCache: 1200,
maxHeightDiskCache: 700,
fit: BoxFit.cover,
placeholder: (_, __) => const _HeroShimmer(radius: radius),
errorWidget: (_, __, ___) =>
@@ -1951,6 +1963,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: img,
memCacheWidth: 300,
memCacheHeight: 200,
maxWidthDiskCache: 600,
maxHeightDiskCache: 400,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
@@ -2147,6 +2161,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: img,
memCacheWidth: 192,
memCacheHeight: 192,
maxWidthDiskCache: 384,
maxHeightDiskCache: 384,
width: 96,
height: double.infinity,
fit: BoxFit.cover,
@@ -2214,6 +2230,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: img,
memCacheWidth: 440,
memCacheHeight: 360,
maxWidthDiskCache: 880,
maxHeightDiskCache: 720,
width: 220,
height: 180,
fit: BoxFit.cover,
@@ -2387,6 +2405,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
imageUrl: img,
memCacheWidth: 800,
memCacheHeight: 400,
maxWidthDiskCache: 1200,
maxHeightDiskCache: 600,
width: double.infinity,
height: 200,
fit: BoxFit.cover,

View File

@@ -332,6 +332,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
maxWidthDiskCache: 1200,
maxHeightDiskCache: 800,
placeholder: (_, __) => Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
@@ -544,6 +546,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
maxWidthDiskCache: 1200,
maxHeightDiskCache: 800,
placeholder: (_, __) => Container(color: theme.dividerColor),
errorWidget: (_, __, ___) => Container(
color: theme.dividerColor,
@@ -847,6 +851,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
maxWidthDiskCache: 1200,
maxHeightDiskCache: 800,
width: double.infinity,
height: double.infinity,
placeholder: (_, __) => Container(
@@ -909,6 +915,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
fit: BoxFit.cover,
memCacheWidth: 800,
memCacheHeight: 500,
maxWidthDiskCache: 1200,
maxHeightDiskCache: 800,
width: double.infinity,
placeholder: (_, __) => Container(
color: theme.dividerColor,
@@ -1567,6 +1575,10 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
imageUrl: imageUrl,
height: 100,
width: 140,
memCacheWidth: 280,
memCacheHeight: 200,
maxWidthDiskCache: 560,
maxHeightDiskCache: 400,
fit: BoxFit.cover,
errorWidget: (_, __, ___) => Container(
height: 100,

View File

@@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:provider/provider.dart';
import '../features/events/services/events_service.dart';
import '../features/events/models/event_models.dart';
import '../core/auth/auth_guard.dart';
import '../features/gamification/providers/gamification_provider.dart';
import '../features/gamification/models/gamification_models.dart';
import '../widgets/skeleton_loader.dart';
@@ -123,9 +124,11 @@ class _ProfileScreenState extends State<ProfileScreen>
_loadProfile();
_startAnimations();
// Load gamification data for profile EP cards
// Load gamification data for profile EP cards — skip for guests (endpoints authed).
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) context.read<GamificationProvider>().loadAll(force: true);
if (mounted && !AuthGuard.isGuest) {
context.read<GamificationProvider>().loadAll(force: true);
}
});
}
@@ -1047,6 +1050,8 @@ class _ProfileScreenState extends State<ProfileScreen>
imageUrl: imageUrl,
memCacheWidth: 120,
memCacheHeight: 120,
maxWidthDiskCache: 240,
maxHeightDiskCache: 240,
width: 60,
height: 60,
fit: BoxFit.cover,
@@ -2552,6 +2557,8 @@ class _ProfileScreenState extends State<ProfileScreen>
imageUrl: imageUrl,
memCacheWidth: 400,
memCacheHeight: 400,
maxWidthDiskCache: 800,
maxHeightDiskCache: 800,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,

View File

@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
class DesktopTopBar extends StatelessWidget {
@@ -108,7 +109,11 @@ class DesktopTopBar extends StatelessWidget {
return CircleAvatar(
radius: 20,
backgroundColor: Colors.grey.shade200,
backgroundImage: NetworkImage(url),
backgroundImage: CachedNetworkImageProvider(
url,
maxWidth: 80,
maxHeight: 80,
),
onBackgroundImageError: (_, __) {},
);
}

View File

@@ -55,6 +55,10 @@ class TierAvatarRing extends StatelessWidget {
return CachedNetworkImage(
imageUrl: _avatarUrl,
memCacheWidth: (size * 2).round(),
memCacheHeight: (size * 2).round(),
maxWidthDiskCache: (size * 4).round(),
maxHeightDiskCache: (size * 4).round(),
imageBuilder: (context, imageProvider) => CircleAvatar(
radius: radius,
backgroundImage: imageProvider,