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:
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: (_, __) {},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user