From 754b04dc059d69be5ff0eeb9d54fd0206ca6d61b Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Sun, 19 Apr 2026 20:03:03 +0530 Subject: [PATCH] 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 --- lib/features/reviews/widgets/review_card.dart | 4 +++ lib/screens/calendar_screen.dart | 4 +++ lib/screens/contribute_screen.dart | 15 +++++++++-- lib/screens/contributor_profile_screen.dart | 12 ++++++--- lib/screens/home_desktop_screen.dart | 6 +++++ lib/screens/home_screen.dart | 26 ++++++++++++++++--- lib/screens/learn_more_screen.dart | 12 +++++++++ lib/screens/profile_screen.dart | 11 ++++++-- lib/widgets/desktop_topbar.dart | 7 ++++- lib/widgets/tier_avatar_ring.dart | 4 +++ 10 files changed, 90 insertions(+), 11 deletions(-) diff --git a/lib/features/reviews/widgets/review_card.dart b/lib/features/reviews/widgets/review_card.dart index 6b9b25a..649e609 100644 --- a/lib/features/reviews/widgets/review_card.dart +++ b/lib/features/reviews/widgets/review_card.dart @@ -125,6 +125,10 @@ class _ReviewCardState extends State { 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), diff --git a/lib/screens/calendar_screen.dart b/lib/screens/calendar_screen.dart index 0160dcf..81e803e 100644 --- a/lib/screens/calendar_screen.dart +++ b/lib/screens/calendar_screen.dart @@ -519,6 +519,8 @@ class _CalendarScreenState extends State { 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 { imageUrl: imgUrl, memCacheWidth: 300, memCacheHeight: 300, + maxWidthDiskCache: 600, + maxHeightDiskCache: 600, width: 100, height: 100, fit: BoxFit.cover, diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index 1522466..bdbaadf 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -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 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(); p.loadAll(); p.loadLeaderboard(); // independent — always fires regardless of loadAll TTL @@ -389,7 +394,7 @@ class _ContributeScreenState extends State 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 } Widget _textField(TextEditingController ctl, String placeholder, { + Key? key, int maxLines = 1, String? Function(String?)? validator, TextInputType? keyboardType, List? inputFormatters, }) { return TextFormField( + key: key, controller: ctl, maxLines: maxLines, keyboardType: keyboardType, @@ -1473,12 +1480,14 @@ class _ContributeScreenState extends State 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 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( diff --git a/lib/screens/contributor_profile_screen.dart b/lib/screens/contributor_profile_screen.dart index 3671ea4..841ad0e 100644 --- a/lib/screens/contributor_profile_screen.dart +++ b/lib/screens/contributor_profile_screen.dart @@ -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 { 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)), ), ), ), diff --git a/lib/screens/home_desktop_screen.dart b/lib/screens/home_desktop_screen.dart index b7204d4..15d8998 100644 --- a/lib/screens/home_desktop_screen.dart +++ b/lib/screens/home_desktop_screen.dart @@ -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, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index b65fb6f..aab9461 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -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 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().refreshUnreadCount(); } } @@ -263,6 +265,8 @@ class _HomeScreenState extends State 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 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 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 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 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 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 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 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 with SingleTickerProviderStateM imageUrl: img, memCacheWidth: 800, memCacheHeight: 400, + maxWidthDiskCache: 1200, + maxHeightDiskCache: 600, width: double.infinity, height: 200, fit: BoxFit.cover, diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart index 179e446..ace004b 100644 --- a/lib/screens/learn_more_screen.dart +++ b/lib/screens/learn_more_screen.dart @@ -332,6 +332,8 @@ class _LearnMoreScreenState extends State 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 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 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 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 with SingleTickerProv imageUrl: imageUrl, height: 100, width: 140, + memCacheWidth: 280, + memCacheHeight: 200, + maxWidthDiskCache: 560, + maxHeightDiskCache: 400, fit: BoxFit.cover, errorWidget: (_, __, ___) => Container( height: 100, diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index bd84a0e..80fd60b 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -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 _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().loadAll(force: true); + if (mounted && !AuthGuard.isGuest) { + context.read().loadAll(force: true); + } }); } @@ -1047,6 +1050,8 @@ class _ProfileScreenState extends State 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 imageUrl: imageUrl, memCacheWidth: 400, memCacheHeight: 400, + maxWidthDiskCache: 800, + maxHeightDiskCache: 800, width: double.infinity, height: double.infinity, fit: BoxFit.cover, diff --git a/lib/widgets/desktop_topbar.dart b/lib/widgets/desktop_topbar.dart index e794e46..20e20b9 100644 --- a/lib/widgets/desktop_topbar.dart +++ b/lib/widgets/desktop_topbar.dart @@ -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: (_, __) {}, ); } diff --git a/lib/widgets/tier_avatar_ring.dart b/lib/widgets/tier_avatar_ring.dart index 2036ea3..7345023 100644 --- a/lib/widgets/tier_avatar_ring.dart +++ b/lib/widgets/tier_avatar_ring.dart @@ -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,