From 28ec9cacb2a02bbb4e90501238a58ae2c025fe3e Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 5 Jul 2026 09:10:46 +0200 Subject: [PATCH] perf(tv): evict non-adjacent season episode pages on low-end hardware MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visited seasons' 200-item pages were retained for the screen's lifetime — irrelevant for a 3-season show, tens of MB of Dart heap for a 30-season one. Low-end TV now keeps the prefetch window (selected season plus/minus one); evicted seasons transparently refetch through the existing unloaded-hub path. Ref #1349 --- lib/screens/media_detail_screen.dart | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index c4ae7a33..444f91f2 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -6,6 +6,7 @@ import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../navigation/profile_navigation_scope.dart'; +import '../services/device_performance.dart'; import '../services/image_cache_service.dart'; import 'package:flutter/services.dart'; import 'package:plezy/utils/platform_detector.dart'; @@ -155,6 +156,14 @@ class _SeasonEpisodePager { _moreLoadsInFlight.remove(seasonId); } + /// Drops cached episode pages for seasons outside [keepSeasonIds]. + /// In-flight sets are left alone — a completing prefetch just re-adds one + /// bounded page. Evicted seasons transparently refetch through the normal + /// unloaded-hub path when refocused. + void retainOnly(Set keepSeasonIds) { + _states.removeWhere((seasonId, _) => !keepSeasonIds.contains(seasonId)); + } + void removeEpisode(String episodeId) { for (final entry in _states.entries.toList()) { _states[entry.key] = entry.value.removeWhere((episode) => episode.id == episodeId); @@ -4023,6 +4032,7 @@ class _MediaDetailScreenState extends State _episodes = List.of(_seasonEpisodePager.stateFor(season.id).items); }); unawaited(_prefetchAdjacentSeasonEpisodePages(seasonIndex)); + _pruneDistantSeasonPages(seasonIndex); return; } @@ -4033,6 +4043,20 @@ class _MediaDetailScreenState extends State }); unawaited(_fetchSeasonEpisodes(seasonIndex)); unawaited(_prefetchAdjacentSeasonEpisodePages(seasonIndex)); + _pruneDistantSeasonPages(seasonIndex); + } + + /// Low-end TV only: keep episode pages for the selected season ±1 (the + /// prefetch window) and drop the rest. Visited 200-item pages otherwise + /// accumulate for the screen's lifetime — irrelevant for a 3-season show, + /// tens of MB of retained heap for a 30-season one. + void _pruneDistantSeasonPages(int seasonIndex) { + if (!DevicePerformance.isLowEndHardware || !PlatformDetector.isTV()) return; + final keep = { + for (var i = seasonIndex - 1; i <= seasonIndex + 1; i++) + if (i >= 0 && i < _seasons.length) _seasons[i].id, + }; + _seasonEpisodePager.retainOnly(keep); } /// What the rail should show in a hub's trailing slot: a spinner while the