perf: fix remaining 11 performance issues across 5 screens
Critical — Image.network → CachedNetworkImage: - home_screen.dart: hero/carousel banner image now cached with placeholder - profile_screen.dart: avatar and event list tile images now cached - calendar_screen.dart: event card images now cached with placeholder High: - profile_screen.dart: TextEditingControllers in dialogs now properly disposed via .then() and after await to prevent memory leaks Medium: - search_screen.dart: shrinkWrap:true → ConstrainedBox(maxHeight:320) + ClampingScrollPhysics for smooth search result scrolling - learn_more_screen.dart: MediaQuery.of(context) cached once per method instead of being called multiple times on every frame Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
// lib/screens/calendar_screen.dart
|
// lib/screens/calendar_screen.dart
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import '../features/events/services/events_service.dart';
|
import '../features/events/services/events_service.dart';
|
||||||
import '../features/events/models/event_models.dart';
|
import '../features/events/models/event_models.dart';
|
||||||
import 'learn_more_screen.dart';
|
import 'learn_more_screen.dart';
|
||||||
@@ -511,7 +512,16 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(14)),
|
||||||
child: imgUrl != null ? Image.network(imgUrl, height: 150, width: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(height: 150, color: theme.dividerColor)) : Container(height: 150, color: theme.dividerColor, child: Icon(Icons.event, size: 44, color: theme.hintColor)),
|
child: imgUrl != null
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: imgUrl,
|
||||||
|
height: 150,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder: (_, __) => Container(height: 150, color: theme.dividerColor),
|
||||||
|
errorWidget: (_, __, ___) => Container(height: 150, color: theme.dividerColor),
|
||||||
|
)
|
||||||
|
: Container(height: 150, color: theme.dividerColor, child: Icon(Icons.event, size: 44, color: theme.hintColor)),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 14),
|
padding: const EdgeInsets.fromLTRB(12, 12, 12, 14),
|
||||||
|
|||||||
@@ -1216,10 +1216,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: img != null && img.isNotEmpty
|
child: img != null && img.isNotEmpty
|
||||||
? Image.network(
|
? CachedNetworkImage(
|
||||||
img,
|
imageUrl: img,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) =>
|
placeholder: (_, __) => Container(
|
||||||
|
decoration: const BoxDecoration(color: Color(0xFF1A2A4A))),
|
||||||
|
errorWidget: (_, __, ___) =>
|
||||||
Container(decoration: AppDecoration.blueGradientRounded(radius)),
|
Container(decoration: AppDecoration.blueGradientRounded(radius)),
|
||||||
)
|
)
|
||||||
: Container(
|
: Container(
|
||||||
|
|||||||
@@ -223,9 +223,10 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final screenHeight = MediaQuery.of(context).size.height;
|
final mediaQuery = MediaQuery.of(context);
|
||||||
|
final screenHeight = mediaQuery.size.height;
|
||||||
final imageHeight = screenHeight * 0.45;
|
final imageHeight = screenHeight * 0.45;
|
||||||
final topPadding = MediaQuery.of(context).padding.top;
|
final topPadding = mediaQuery.padding.top;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
@@ -355,6 +356,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Widget _buildLoadingShimmer(ThemeData theme) {
|
Widget _buildLoadingShimmer(ThemeData theme) {
|
||||||
|
final shimmerHeight = MediaQuery.of(context).size.height;
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
@@ -363,7 +365,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
children: [
|
children: [
|
||||||
// Placeholder image
|
// Placeholder image
|
||||||
Container(
|
Container(
|
||||||
height: MediaQuery.of(context).size.height * 0.42,
|
height: shimmerHeight * 0.42,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.dividerColor.withOpacity(0.3),
|
color: theme.dividerColor.withOpacity(0.3),
|
||||||
borderRadius: BorderRadius.circular(28),
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
@@ -273,6 +274,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
final result = await showDialog<String?>(
|
final result = await showDialog<String?>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) {
|
builder: (ctx) {
|
||||||
|
// Note: ctl is disposed after dialog closes below
|
||||||
final theme = Theme.of(ctx);
|
final theme = Theme.of(ctx);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Enter image path or URL'),
|
title: const Text('Enter image path or URL'),
|
||||||
@@ -305,6 +307,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ctl.dispose();
|
||||||
if (result == null || result.isEmpty) return;
|
if (result == null || result.isEmpty) return;
|
||||||
await _saveProfile(_username, _email, result);
|
await _saveProfile(_username, _email, result);
|
||||||
}
|
}
|
||||||
@@ -318,6 +321,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (ctx) {
|
builder: (ctx) {
|
||||||
|
// nameCtl and emailCtl are disposed via .then() below
|
||||||
final theme = Theme.of(ctx);
|
final theme = Theme.of(ctx);
|
||||||
return DraggableScrollableSheet(
|
return DraggableScrollableSheet(
|
||||||
expand: false,
|
expand: false,
|
||||||
@@ -419,7 +423,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
).then((_) {
|
||||||
|
nameCtl.dispose();
|
||||||
|
emailCtl.dispose();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────── Avatar builder (reused, with size param) ─────────
|
// ───────── Avatar builder (reused, with size param) ─────────
|
||||||
@@ -428,11 +435,14 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
final path = _profileImage.trim();
|
final path = _profileImage.trim();
|
||||||
if (path.startsWith('http')) {
|
if (path.startsWith('http')) {
|
||||||
return ClipOval(
|
return ClipOval(
|
||||||
child: Image.network(path,
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: path,
|
||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) =>
|
placeholder: (_, __) =>
|
||||||
|
Container(width: size, height: size, color: const Color(0xFFE5E7EB)),
|
||||||
|
errorWidget: (_, __, ___) =>
|
||||||
Icon(Icons.person, size: size / 2, color: Colors.grey)));
|
Icon(Icons.person, size: size / 2, color: Colors.grey)));
|
||||||
}
|
}
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
@@ -497,11 +507,16 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
if (imageUrl.startsWith('http')) {
|
if (imageUrl.startsWith('http')) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.network(imageUrl,
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
placeholder: (_, __) => Container(
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
color: const Color(0xFFE5E7EB)),
|
||||||
|
errorWidget: (_, __, ___) => Container(
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
color: theme.dividerColor,
|
color: theme.dividerColor,
|
||||||
|
|||||||
@@ -305,9 +305,11 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
child: Center(child: Text('No results found', style: TextStyle(color: Colors.grey[500]))),
|
child: Center(child: Text('No results found', style: TextStyle(color: Colors.grey[500]))),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ListView.separated(
|
ConstrainedBox(
|
||||||
shrinkWrap: true,
|
constraints: const BoxConstraints(maxHeight: 320),
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
child: ListView.separated(
|
||||||
|
shrinkWrap: false,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
itemCount: _searchResults.length,
|
itemCount: _searchResults.length,
|
||||||
separatorBuilder: (_, __) => Divider(color: Colors.grey[200], height: 1),
|
separatorBuilder: (_, __) => Divider(color: Colors.grey[200], height: 1),
|
||||||
itemBuilder: (ctx, idx) {
|
itemBuilder: (ctx, idx) {
|
||||||
@@ -326,6 +328,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF1A1A2E))),
|
const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF1A1A2E))),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
Reference in New Issue
Block a user