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:
2026-03-30 10:05:23 +05:30
parent 87cc56dc64
commit b55f02e057
8 changed files with 357 additions and 82 deletions

View File

@@ -1,4 +1,6 @@
// lib/features/events/models/event_models.dart
import '../../../core/api/api_endpoints.dart';
class EventTypeModel {
final int id;
final String name;
@@ -6,11 +8,18 @@ class EventTypeModel {
EventTypeModel({required this.id, required this.name, this.iconUrl});
/// Resolve a relative media path (e.g. `/media/...`) to a full URL.
static String? _resolveMediaUrl(String? raw) {
if (raw == null || raw.isEmpty) return null;
if (raw.startsWith('http://') || raw.startsWith('https://')) return raw;
return '${ApiEndpoints.mediaBaseUrl}$raw';
}
factory EventTypeModel.fromJson(Map<String, dynamic> j) {
return EventTypeModel(
id: j['id'] as int,
name: (j['event_type'] ?? j['name'] ?? '') as String,
iconUrl: (j['event_type_icon'] ?? j['icon_url']) as String?,
iconUrl: _resolveMediaUrl((j['event_type_icon'] ?? j['icon_url']) as String?),
);
}
}
@@ -24,7 +33,7 @@ class EventImageModel {
factory EventImageModel.fromJson(Map<String, dynamic> j) {
return EventImageModel(
isPrimary: j['is_primary'] == true,
image: (j['image'] ?? '') as String,
image: EventTypeModel._resolveMediaUrl(j['image'] as String?) ?? '',
);
}
}
@@ -129,7 +138,7 @@ class EventModel {
place: (j['place'] ?? j['venue_name']) as String?,
isBookable: j['is_bookable'] == null ? true : (j['is_bookable'] == true || j['is_bookable'].toString().toLowerCase() == 'true'),
eventTypeId: j['event_type'] is int ? j['event_type'] as int : (j['event_type'] != null ? int.tryParse(j['event_type'].toString()) : null),
thumbImg: j['thumb_img'] as String?,
thumbImg: EventTypeModel._resolveMediaUrl(j['thumb_img'] as String?),
images: imgs,
importantInformation: j['important_information'] as String?,
venueName: j['venue_name'] as String?,

View File

@@ -1,5 +1,4 @@
// lib/features/events/services/events_service.dart
import 'package:intl/intl.dart';
import '../../../core/api/api_client.dart';
import '../../../core/api/api_endpoints.dart';
import '../models/event_models.dart';
@@ -7,27 +6,62 @@ import '../models/event_models.dart';
class EventsService {
final ApiClient _api = ApiClient();
// ---------------------------------------------------------------------------
// In-memory caches with TTL
// ---------------------------------------------------------------------------
static List<EventTypeModel>? _cachedTypes;
static DateTime? _typesCacheTime;
static const _typesCacheTTL = Duration(minutes: 30);
static List<EventModel>? _cachedAllEvents;
static DateTime? _eventsCacheTime;
static const _eventsCacheTTL = Duration(minutes: 5);
/// Get event types (POST to /events/type-list/)
/// Cached for 30 minutes since event types rarely change.
Future<List<EventTypeModel>> getEventTypes() async {
if (_cachedTypes != null &&
_typesCacheTime != null &&
DateTime.now().difference(_typesCacheTime!) < _typesCacheTTL) {
return _cachedTypes!;
}
final res = await _api.post(ApiEndpoints.eventTypes, requiresAuth: false);
final list = <EventTypeModel>[];
final data = res['event_types'] ?? res['event_types'] ?? res;
final data = res['event_types'] ?? res;
if (data is List) {
for (final e in data) {
if (e is Map<String, dynamic>) list.add(EventTypeModel.fromJson(e));
}
} else if (res['event_types'] is List) {
for (final e in res['event_types']) {
list.add(EventTypeModel.fromJson(Map<String, dynamic>.from(e)));
}
}
_cachedTypes = list;
_typesCacheTime = DateTime.now();
return list;
}
/// Get events filtered by pincode (POST to /events/pincode-events/)
/// Use pincode='all' to fetch all events.
Future<List<EventModel>> getEventsByPincode(String pincode) async {
final res = await _api.post(ApiEndpoints.eventsByPincode, body: {'pincode': pincode}, requiresAuth: false);
/// Get events filtered by pincode with pagination.
/// [page] starts at 1. [pageSize] defaults to 50.
/// Returns a list of events for the requested page.
Future<List<EventModel>> getEventsByPincode(String pincode, {int page = 1, int pageSize = 50, int perType = 5}) async {
// Use cache for 'all' pincode queries (first page only for initial load)
if (pincode == 'all' &&
page == 1 &&
_cachedAllEvents != null &&
_eventsCacheTime != null &&
DateTime.now().difference(_eventsCacheTime!) < _eventsCacheTTL) {
return _cachedAllEvents!;
}
final Map<String, dynamic> body = {'pincode': pincode, 'page': page, 'page_size': pageSize};
// Diverse mode: fetch a few events per type so all categories are represented
if (perType > 0 && page == 1) body['per_type'] = perType;
final res = await _api.post(
ApiEndpoints.eventsByPincode,
body: body,
requiresAuth: false,
);
final list = <EventModel>[];
final events = res['events'] ?? res['data'] ?? [];
if (events is List) {
@@ -35,6 +69,11 @@ class EventsService {
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
}
}
if (pincode == 'all' && page == 1) {
_cachedAllEvents = list;
_eventsCacheTime = DateTime.now();
}
return list;
}
@@ -45,23 +84,21 @@ class EventsService {
}
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
/// Accepts month string and year int.
/// Returns Map with 'dates' (list of YYYY-MM-DD) and 'date_events' (list with counts).
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
// expected keys: dates, total_number_of_events, date_events
return res;
}
/// Convenience: get events for a specific date (YYYY-MM-DD)
/// Convenience: get events for a specific date (YYYY-MM-DD).
/// Uses the cached events list when available to avoid redundant API calls.
Future<List<EventModel>> getEventsForDate(String date) async {
// Simplest approach: hit pincode-events with filter or hit events-by-month-year and then
// query event-details for events of that date. Assuming backend doesn't provide direct endpoint,
// we'll call eventsByPincode('all') and filter locally by date — acceptable for demo/small datasets.
final all = await getEventsByPincode('all');
return all.where((e) {
try {
return e.startDate == date || e.endDate == date || (DateTime.parse(e.startDate).isBefore(DateTime.parse(date)) && DateTime.parse(e.endDate).isAfter(DateTime.parse(date)));
return e.startDate == date ||
e.endDate == date ||
(DateTime.parse(e.startDate).isBefore(DateTime.parse(date)) &&
DateTime.parse(e.endDate).isAfter(DateTime.parse(date)));
} catch (_) {
return false;
}