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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// lib/screens/calendar_screen.dart
|
||||
import 'package:flutter/material.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/models/event_models.dart';
|
||||
import 'learn_more_screen.dart';
|
||||
@@ -511,7 +512,16 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
||||
children: [
|
||||
ClipRRect(
|
||||
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: const EdgeInsets.fromLTRB(12, 12, 12, 14),
|
||||
|
||||
@@ -1216,10 +1216,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: img != null && img.isNotEmpty
|
||||
? Image.network(
|
||||
img,
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) =>
|
||||
placeholder: (_, __) => Container(
|
||||
decoration: const BoxDecoration(color: Color(0xFF1A2A4A))),
|
||||
errorWidget: (_, __, ___) =>
|
||||
Container(decoration: AppDecoration.blueGradientRounded(radius)),
|
||||
)
|
||||
: 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 topPadding = MediaQuery.of(context).padding.top;
|
||||
final topPadding = mediaQuery.padding.top;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.scaffoldBackgroundColor,
|
||||
@@ -355,6 +356,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Widget _buildLoadingShimmer(ThemeData theme) {
|
||||
final shimmerHeight = MediaQuery.of(context).size.height;
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
@@ -363,7 +365,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
children: [
|
||||
// Placeholder image
|
||||
Container(
|
||||
height: MediaQuery.of(context).size.height * 0.42,
|
||||
height: shimmerHeight * 0.42,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.dividerColor.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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:image_picker/image_picker.dart';
|
||||
@@ -273,6 +274,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
final result = await showDialog<String?>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
// Note: ctl is disposed after dialog closes below
|
||||
final theme = Theme.of(ctx);
|
||||
return AlertDialog(
|
||||
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;
|
||||
await _saveProfile(_username, _email, result);
|
||||
}
|
||||
@@ -318,6 +321,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) {
|
||||
// nameCtl and emailCtl are disposed via .then() below
|
||||
final theme = Theme.of(ctx);
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
@@ -419,7 +423,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
).then((_) {
|
||||
nameCtl.dispose();
|
||||
emailCtl.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
// ───────── Avatar builder (reused, with size param) ─────────
|
||||
@@ -428,11 +435,14 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
final path = _profileImage.trim();
|
||||
if (path.startsWith('http')) {
|
||||
return ClipOval(
|
||||
child: Image.network(path,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: path,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
placeholder: (_, __) =>
|
||||
Container(width: size, height: size, color: const Color(0xFFE5E7EB)),
|
||||
errorWidget: (_, __, ___) =>
|
||||
Icon(Icons.person, size: size / 2, color: Colors.grey)));
|
||||
}
|
||||
if (kIsWeb) {
|
||||
@@ -497,11 +507,16 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
if (imageUrl.startsWith('http')) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Image.network(imageUrl,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => Container(
|
||||
placeholder: (_, __) => Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
color: const Color(0xFFE5E7EB)),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
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]))),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 320),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: false,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: _searchResults.length,
|
||||
separatorBuilder: (_, __) => Divider(color: Colors.grey[200], height: 1),
|
||||
itemBuilder: (ctx, idx) {
|
||||
@@ -326,6 +328,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF1A1A2E))),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Reference in New Issue
Block a user