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

@@ -28,25 +28,10 @@ class _MobileDashboardScreenState extends ConsumerState<MobileDashboardScreen> {
Widget build(BuildContext context) {
final currentIndex = ref.watch(mobileDashboardIndexProvider);
// Determine which tab to show
Widget currentTab;
switch (currentIndex) {
case 0:
currentTab = MobileLiveTVTab(playlist: widget.playlist);
break;
case 1:
currentTab = MobileMoviesTab(playlist: widget.playlist);
break;
case 2:
currentTab = MobileSeriesTab(playlist: widget.playlist);
break;
case 3:
currentTab = const MobileSettingsTab();
break;
default:
currentTab = MobileLiveTVTab(playlist: widget.playlist);
}
// [P0-3 FIX] Use IndexedStack to preserve tab state and scroll position
// Instead of destroying/recreating tabs on each switch, keep all tabs alive in memory
// This enables instant tab switching and preserves user scroll position, search state, etc.
// The AutomaticKeepAliveClientMixin in each tab will keep them in the widget tree
return Theme(
data: MobileTheme.darkTheme,
child: MobileScaffold(
@@ -54,7 +39,30 @@ class _MobileDashboardScreenState extends ConsumerState<MobileDashboardScreen> {
onIndexChanged: (index) {
ref.read(mobileDashboardIndexProvider.notifier).state = index;
},
child: currentTab,
child: IndexedStack(
index: currentIndex,
children: [
// Tab 0: Live TV - stays alive to preserve channel list scroll position
MobileLiveTVTab(
key: const PageStorageKey('live_tv_tab'),
playlist: widget.playlist,
),
// Tab 1: Movies - stays alive to preserve search and filter state
MobileMoviesTab(
key: const PageStorageKey('movies_tab'),
playlist: widget.playlist,
),
// Tab 2: Series - stays alive to preserve series browsing state
MobileSeriesTab(
key: const PageStorageKey('series_tab'),
playlist: widget.playlist,
),
// Tab 3: Settings - stays alive to preserve settings scroll position
const MobileSettingsTab(
key: PageStorageKey('settings_tab'),
),
],
),
),
);
}
@@ -459,6 +459,8 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_player.stop();
_player.dispose();
_xtreamService?.dispose();
// [P1-3 FIX] Dispose all focus nodes ONCE (was being disposed twice, causing crash)
_playPauseFocusNode.dispose();
_prevFocusNode.dispose();
_nextFocusNode.dispose();
@@ -475,12 +477,6 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
// Dispose focus nodes
_playPauseFocusNode.dispose();
_prevFocusNode.dispose();
_nextFocusNode.dispose();
_backFocusNode.dispose();
super.dispose();
}