perf: optimize loading time — paginated API, slim payloads, local category filtering
Backend: Rewrote EventListAPI to query per-type with DB-level LIMIT instead of loading all 734 events into memory. Added slim serializer (32KB vs 154KB). Added DB indexes on event_type_id and pincode. Frontend: Category chips now filter locally from _allEvents (instant, no API call). Top Events and category sections always show all types regardless of selected category. Added TTL caching for event types (30min) and events (5min). Reduced API timeout from 30s to 10s. Added memCacheHeight to all CachedNetworkImage widgets. Batched setState calls from 5 to 2 during startup. Cached _eventDates getter. Switched baseUrl to em.eventifyplus.com (Django via Nginx+SSL). Added initialEvent param to LearnMoreScreen for instant detail views. Resolved relative media URLs for category icons. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,7 @@ class _HomeContentState extends State<_HomeContent>
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
memCacheWidth: 1400,
|
||||
memCacheHeight: 800,
|
||||
placeholder: (_, __) => Container(
|
||||
color: const Color(0xFF0A0E1A),
|
||||
),
|
||||
@@ -527,6 +528,7 @@ class _HomeContentState extends State<_HomeContent>
|
||||
imageUrl: img,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 1400,
|
||||
memCacheHeight: 800,
|
||||
)
|
||||
else
|
||||
Container(color: const Color(0xFF0A0E1A)),
|
||||
|
||||
@@ -36,6 +36,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
final EventsService _eventsService = EventsService();
|
||||
|
||||
// backend-driven
|
||||
List<EventModel> _allEvents = []; // master copy, never filtered
|
||||
List<EventModel> _events = [];
|
||||
List<EventTypeModel> _types = [];
|
||||
int _selectedTypeId = -1; // -1 == All
|
||||
@@ -87,7 +88,6 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
final coordMatch = RegExp(r'^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$').firstMatch(storedLocation);
|
||||
if (coordMatch != null) {
|
||||
_location = 'Current Location';
|
||||
setState(() {});
|
||||
// Reverse geocode in background to get actual place name
|
||||
_reverseGeocodeAndSave(
|
||||
double.parse(coordMatch.group(1)!),
|
||||
@@ -110,17 +110,19 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_types = types;
|
||||
_allEvents = events;
|
||||
_events = events;
|
||||
_selectedTypeId = -1;
|
||||
_cachedFilteredEvents = null; // invalidate cache
|
||||
_cachedFilteredEvents = null;
|
||||
_cachedEventDates = null;
|
||||
_loading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,6 +547,52 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
List<EventModel>? _cachedFilteredEvents;
|
||||
String _cachedFilterKey = '';
|
||||
|
||||
// Cached event dates for calendar dots
|
||||
Set<DateTime>? _cachedEventDates;
|
||||
|
||||
/// Returns all events filtered by date only (ignores category selection).
|
||||
/// Used by Top Events and category sections so they always show all types.
|
||||
List<EventModel> get _allFilteredByDate {
|
||||
if (_selectedDateFilter.isEmpty) return _allEvents;
|
||||
// Reuse the same date-filter logic as _filteredEvents but on _allEvents
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
DateTime filterStart;
|
||||
DateTime filterEnd;
|
||||
switch (_selectedDateFilter) {
|
||||
case 'Today':
|
||||
filterStart = today;
|
||||
filterEnd = today;
|
||||
break;
|
||||
case 'Tomorrow':
|
||||
filterStart = today.add(const Duration(days: 1));
|
||||
filterEnd = filterStart;
|
||||
break;
|
||||
case 'This week':
|
||||
filterStart = today;
|
||||
filterEnd = today.add(Duration(days: 7 - today.weekday));
|
||||
break;
|
||||
case 'Date':
|
||||
if (_selectedCustomDate == null) return _allEvents;
|
||||
filterStart = DateTime(_selectedCustomDate!.year, _selectedCustomDate!.month, _selectedCustomDate!.day);
|
||||
filterEnd = filterStart;
|
||||
break;
|
||||
default:
|
||||
return _allEvents;
|
||||
}
|
||||
return _allEvents.where((e) {
|
||||
try {
|
||||
final s = DateTime.parse(e.startDate);
|
||||
final eEnd = DateTime.parse(e.endDate);
|
||||
final eStart = DateTime(s.year, s.month, s.day);
|
||||
final eEndDay = DateTime(eEnd.year, eEnd.month, eEnd.day);
|
||||
return !eEndDay.isBefore(filterStart) && !eStart.isAfter(filterEnd);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Returns the subset of [_events] that match the active date-filter chip.
|
||||
/// Uses caching to avoid re-parsing dates on every access.
|
||||
List<EventModel> get _filteredEvents {
|
||||
@@ -887,9 +935,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
}
|
||||
|
||||
/// Collect all event dates (start + end range) to show dots on the calendar.
|
||||
/// Cached to avoid re-parsing on every calendar open.
|
||||
Set<DateTime> get _eventDates {
|
||||
if (_cachedEventDates != null) return _cachedEventDates!;
|
||||
final dates = <DateTime>{};
|
||||
for (final e in _events) {
|
||||
for (final e in _allEvents) {
|
||||
try {
|
||||
final start = DateTime.parse(e.startDate);
|
||||
final end = DateTime.parse(e.endDate);
|
||||
@@ -901,6 +951,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
_cachedEventDates = dates;
|
||||
return dates;
|
||||
}
|
||||
|
||||
@@ -1318,6 +1369,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
memCacheWidth: 700,
|
||||
memCacheHeight: 400,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (_, __) => const _HeroShimmer(radius: radius),
|
||||
errorWidget: (_, __, ___) =>
|
||||
@@ -1498,18 +1550,18 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: _filteredEvents.isEmpty && _loading
|
||||
child: _allFilteredByDate.isEmpty && _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _filteredEvents.isEmpty
|
||||
: _allFilteredByDate.isEmpty
|
||||
? Center(child: Text(
|
||||
_selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found',
|
||||
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
||||
))
|
||||
: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _filteredEvents.length,
|
||||
itemCount: _allFilteredByDate.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) => _buildTopEventCard(_filteredEvents[index]),
|
||||
itemBuilder: (context, index) => _buildTopEventCard(_allFilteredByDate[index]),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -1565,42 +1617,30 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Event sections by type
|
||||
if (_selectedTypeId == -1) ...[
|
||||
if (_loading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (_filteredEvents.isEmpty && _selectedDateFilter.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Center(child: Text(
|
||||
'No events for "$_selectedDateFilter"',
|
||||
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
||||
)),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
for (final t in _types)
|
||||
if (_filteredEvents.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
|
||||
_buildTypeSection(t),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
if (_loading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: _filteredEvents.map((e) => _buildFullWidthCard(e)).toList(),
|
||||
),
|
||||
],
|
||||
// Event sections by type — always show ALL categories
|
||||
if (_loading)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
)
|
||||
else if (_allFilteredByDate.isEmpty && _selectedDateFilter.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Center(child: Text(
|
||||
'No events for "$_selectedDateFilter"',
|
||||
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
||||
)),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
for (final t in _types)
|
||||
if (_allFilteredByDate.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
|
||||
_buildTypeSection(t),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
// Bottom padding for nav bar
|
||||
const SizedBox(height: 100),
|
||||
@@ -1680,6 +1720,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
memCacheWidth: 300,
|
||||
memCacheHeight: 200,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
@@ -1740,7 +1781,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
/// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns).
|
||||
Widget _buildTypeSection(EventTypeModel type) {
|
||||
final theme = Theme.of(context);
|
||||
final eventsForType = _filteredEvents.where((e) => e.eventTypeId == type.id).toList();
|
||||
final eventsForType = _allFilteredByDate.where((e) => e.eventTypeId == type.id).toList();
|
||||
final n = eventsForType.length;
|
||||
|
||||
// Header row
|
||||
@@ -1874,6 +1915,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
? CachedNetworkImage(
|
||||
imageUrl: img,
|
||||
memCacheWidth: 192,
|
||||
memCacheHeight: 192,
|
||||
width: 96,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
@@ -2241,18 +2283,12 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
||||
return Icons.event;
|
||||
}
|
||||
|
||||
void _onSelectType(int id) async {
|
||||
void _onSelectType(int id) {
|
||||
setState(() {
|
||||
_selectedTypeId = id;
|
||||
_events = id == -1 ? List.from(_allEvents) : _allEvents.where((e) => e.eventTypeId == id).toList();
|
||||
_cachedFilteredEvents = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final all = await _eventsService.getEventsByPincode(_pincode);
|
||||
final filtered = id == -1 ? all : all.where((e) => e.eventTypeId == id).toList();
|
||||
if (mounted) setState(() { _events = filtered; _cachedFilteredEvents = null; });
|
||||
} catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
}
|
||||
|
||||
String _getShortEmailLabel() {
|
||||
|
||||
@@ -265,6 +265,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
CachedNetworkImage(
|
||||
imageUrl: heroImage,
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 800,
|
||||
memCacheHeight: 500,
|
||||
placeholder: (_, __) => Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -471,6 +473,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
CachedNetworkImage(
|
||||
imageUrl: images[i],
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 800,
|
||||
memCacheHeight: 500,
|
||||
placeholder: (_, __) => Container(color: theme.dividerColor),
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
color: theme.dividerColor,
|
||||
@@ -724,6 +728,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
builder: (context, currentPage, _) => CachedNetworkImage(
|
||||
imageUrl: images[currentPage],
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 800,
|
||||
memCacheHeight: 500,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
placeholder: (_, __) => Container(
|
||||
@@ -782,6 +788,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
itemBuilder: (_, i) => CachedNetworkImage(
|
||||
imageUrl: images[i],
|
||||
fit: BoxFit.cover,
|
||||
memCacheWidth: 800,
|
||||
memCacheHeight: 500,
|
||||
width: double.infinity,
|
||||
placeholder: (_, __) => Container(
|
||||
color: theme.dividerColor,
|
||||
|
||||
Reference in New Issue
Block a user