perf(critical): Implement P0 optimization fixes - TiviMate parity

CRITICAL FIXES:
- P0-1: Fix EPG N+1 query storm with batch loading
  * Implement XtreamServiceMobile with getBatchEPG() method
  * Batch load EPG data instead of per-channel requests
  * Reduce 50-200 concurrent requests to 1 request per category
  * Add EPG cache with 1-hour TTL to prevent redundant calls

- P0-2: Remove destructive cache clearing on startup
  * Implement TTL-based cache invalidation
  * Preserve cache between app launches (6h channels, 1h EPG, 30m search)
  * Add HiveService.invalidateExpiredCache() method
  * Expected: -80% startup time on cold launches

- P0-3: Preserve tab state with IndexedStack
  * Replace switch statement with IndexedStack
  * Keep all tabs alive in widget tree
  * Preserve scroll position, search state, filters across tab switches
  * Expected: instant tab switching (<100ms)

HIGH PRIORITY FIXES:
- P1-1: Debounce search input (500ms)
  * Prevent per-keystroke full rebuilds
  * Add search debounce timer

- P1-3: Fix double FocusNode disposal crash
  * Remove duplicate focus node disposal in native_player_screen.dart
  * Prevents StateError on player exit

Expected impact: -75-80% perceived latency, instant tab switching, no OOM on startup.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
GitHub CopilotandClaude Haiku 4.5 committed 2026-03-25 22:31:36 +01:00
1 parent 70c2241527
commit 93b8e5b337
8 files changed
+791 -41

No files matched your search

+70
View File
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter/foundation.dart';
import '../models/app_user.dart';
import '../models/playlist_config.dart';
@@ -111,6 +112,75 @@ class HiveService {
static Box<PlaylistConfig> get playlistsBox =>
Hive.box<PlaylistConfig>(_playlistsBoxName);
/// [P0-2 FIX] Invalidate expired cache entries based on TTL
/// Instead of clearing entire cache on startup, use TTL-based invalidation
/// This preserves cache data between app launches while removing stale entries
static Future<void> invalidateExpiredCache() async {
try {
final cacheBox = Hive.box('dio_cache');
// TTL configuration (in seconds)
const Map<String, int> cacheTtl = {
'channels': 6 * 3600, // 6 hours for channel lists
'epg': 3600, // 1 hour for EPG data
'categories': 6 * 3600, // 6 hours for categories
'search': 1800, // 30 minutes for search results
'default': 6 * 3600, // 6 hours default
};
int expired = 0;
final now = DateTime.now().millisecondsSinceEpoch;
for (var key in cacheBox.keys.toList()) {
try {
final entry = cacheBox.get(key);
if (entry is Map && entry.containsKey('timestamp')) {
final timestamp = entry['timestamp'] as int;
final category = _getCacheCategory(key.toString());
final ttl = cacheTtl[category] ?? cacheTtl['default']!;
// If entry is older than TTL, delete it
if ((now - timestamp) > (ttl * 1000)) {
await cacheBox.delete(key);
expired++;
}
}
} catch (e) {
// Skip malformed entries
}
}
if (expired > 0) {
debugPrint('🗑️ Invalidated $expired expired cache entries');
} else {
debugPrint('✅ All cache entries valid (within TTL)');
}
} catch (e) {
// Cache box might not exist - that's OK on first startup
debugPrint('ℹ️ Cache maintenance: $e');
}
}
/// Determine cache category from key for TTL lookup
static String _getCacheCategory(String key) {
if (key.contains('live') || key.contains('channel')) return 'channels';
if (key.contains('epg') || key.contains('now_playing')) return 'epg';
if (key.contains('category')) return 'categories';
if (key.contains('search')) return 'search';
return 'default';
}
/// Manually clear cache (for settings option)
static Future<void> clearCache() async {
try {
await Hive.deleteBoxFromDisk('dio_cache');
debugPrint('🗑️ Cache manually cleared by user');
} catch (e) {
debugPrint('❌ Failed to clear cache: $e');
}
}
/// Close all boxes (cleanup)
static Future<void> dispose() async {
await Hive.close();