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:
2026-03-18 16:28:32 +05:30
parent 2aa05366ad
commit 002ed3ee98
5 changed files with 47 additions and 15 deletions

View File

@@ -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),

View File

@@ -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(

View File

@@ -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),

View File

@@ -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,

View File

@@ -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),