From ebbcc60e13739949554fbec73728aa44a4275063 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:44:15 +0200 Subject: [PATCH] perf(screens): cache Theme.of in hot media detail/discover paths --- lib/screens/discover_screen.dart | 90 ++++++++++++++-------------- lib/screens/media_detail_screen.dart | 74 +++++++++++------------ 2 files changed, 79 insertions(+), 85 deletions(-) diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index 4871a377..3516140d 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -871,7 +871,7 @@ class _DiscoverScreenState extends State Widget _buildOverlaidAppBar() { final statusBarHeight = MediaQuery.paddingOf(context).top; - return Container( + return DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -902,6 +902,7 @@ class _DiscoverScreenState extends State builder: (context, watchTogether, companionRemote, _) { final isDesktop = PlatformDetector.shouldActAsRemoteHost(context); final userProvider = context.watch(); + final colorScheme = Theme.of(context).colorScheme; return FocusableActionBar( key: _actionBarKey, @@ -919,7 +920,7 @@ class _DiscoverScreenState extends State icon: AppIcon( Symbols.group_rounded, fill: watchTogether.isInSession ? 1 : 0, - color: watchTogether.isInSession ? Theme.of(context).colorScheme.primary : Colors.white, + color: watchTogether.isInSession ? colorScheme.primary : Colors.white, ), onPressed: () => Navigator.push( context, @@ -934,13 +935,13 @@ class _DiscoverScreenState extends State child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, + color: colorScheme.primary, borderRadius: const BorderRadius.all(Radius.circular(8)), ), child: Text( '${watchTogether.participantCount}', style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, + color: colorScheme.onPrimary, fontSize: 10, fontWeight: FontWeight.bold, ), @@ -968,9 +969,7 @@ class _DiscoverScreenState extends State icon: AppIcon( Symbols.phone_android_rounded, fill: companionRemote.isConnected ? 1 : 0, - color: companionRemote.isConnected - ? Theme.of(context).colorScheme.primary - : Colors.white, + color: companionRemote.isConnected ? colorScheme.primary : Colors.white, ), onPressed: () { if (isDesktop) { @@ -1060,8 +1059,9 @@ class _DiscoverScreenState extends State final duplicateHubTitles = _getDuplicateHubTitles(); final bottomPadding = MediaQuery.paddingOf(context).bottom; + final theme = Theme.of(context); return Material( - color: Theme.of(context).scaffoldBackgroundColor, + color: theme.scaffoldBackgroundColor, child: Stack( children: [ CustomScrollView( @@ -1143,7 +1143,7 @@ class _DiscoverScreenState extends State width: 200, height: 24, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, + color: theme.colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.all(Radius.circular(4)), ), ), @@ -1325,6 +1325,8 @@ class _DiscoverScreenState extends State final showName = heroItem.grandparentTitle ?? heroItem.displayTitle; final screenWidth = MediaQuery.sizeOf(context).width; final isLargeScreen = ScreenBreakpoints.isWideTabletOrLarger(screenWidth); + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; // Determine content type label for chip final contentTypeLabel = heroItem.isMovie ? t.discover.movie : t.discover.tvShow; @@ -1409,7 +1411,7 @@ class _DiscoverScreenState extends State ), ) else - ColoredBox(color: Theme.of(context).colorScheme.surfaceContainerHighest), + ColoredBox(color: colorScheme.surfaceContainerHighest), // Gradient Overlay - blends into scaffold background Positioned( @@ -1473,39 +1475,39 @@ class _DiscoverScreenState extends State fit: BoxFit.contain, memCacheWidth: (400 * dpr).clamp(200, 800).round(), alignment: isLargeScreen ? Alignment.bottomLeft : Alignment.bottomCenter, - placeholder: (context, url) => Align( - alignment: isLargeScreen ? Alignment.centerLeft : Alignment.center, - child: Text( - showName, - style: Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3), - fontWeight: FontWeight.bold, - shadows: [ - Shadow( - color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), - blurRadius: 8, - ), - ], - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: isLargeScreen ? TextAlign.left : TextAlign.center, - ), - ), - errorWidget: (context, url, error) { - // Fallback to text if logo fails to load + placeholder: (context, url) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; return Align( alignment: isLargeScreen ? Alignment.centerLeft : Alignment.center, child: Text( showName, - style: Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface, + style: theme.textTheme.displaySmall?.copyWith( + color: colorScheme.onSurface.withValues(alpha: 0.3), fontWeight: FontWeight.bold, shadows: [ - Shadow( - color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), - blurRadius: 8, - ), + Shadow(color: colorScheme.surface.withValues(alpha: 0.8), blurRadius: 8), + ], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: isLargeScreen ? TextAlign.left : TextAlign.center, + ), + ); + }, + errorWidget: (context, url, error) { + // Fallback to text if logo fails to load + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + return Align( + alignment: isLargeScreen ? Alignment.centerLeft : Alignment.center, + child: Text( + showName, + style: theme.textTheme.displaySmall?.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.bold, + shadows: [ + Shadow(color: colorScheme.surface.withValues(alpha: 0.8), blurRadius: 8), ], ), maxLines: 2, @@ -1524,12 +1526,10 @@ class _DiscoverScreenState extends State else Text( showName, - style: Theme.of(context).textTheme.displaySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface, + style: theme.textTheme.displaySmall?.copyWith( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, - shadows: [ - Shadow(color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), blurRadius: 8), - ], + shadows: [Shadow(color: colorScheme.surface.withValues(alpha: 0.8), blurRadius: 8)], ), maxLines: 2, overflow: TextOverflow.ellipsis, @@ -1565,7 +1565,7 @@ class _DiscoverScreenState extends State style: TextStyle( color: isLargeScreen ? Colors.white.withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + : colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 14, height: 1.4, ), @@ -1575,7 +1575,7 @@ class _DiscoverScreenState extends State text: 'S${heroItem.parentIndex}, E${heroItem.index}: ', style: TextStyle( fontWeight: FontWeight.bold, - color: isLargeScreen ? Colors.white : Theme.of(context).colorScheme.onSurface, + color: isLargeScreen ? Colors.white : colorScheme.onSurface, ), ), TextSpan( @@ -1599,7 +1599,7 @@ class _DiscoverScreenState extends State style: TextStyle( color: isLargeScreen ? Colors.white.withValues(alpha: 0.7) - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + : colorScheme.onSurface.withValues(alpha: 0.7), fontSize: 14, height: 1.4, ), diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index d7e1b505..fa5da6df 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -413,6 +413,7 @@ class _MediaDetailScreenState extends State /// Build radial progress indicator for download button /// If progressPercent is null or 0, shows indeterminate spinner Widget _buildRadialProgress(double? progressPercent) { + final colorScheme = Theme.of(context).colorScheme; return SizedBox( width: 20, height: 20, @@ -424,13 +425,13 @@ class _MediaDetailScreenState extends State CircularProgressIndicator( value: 1.0, strokeWidth: 2.0, - valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.primary.withValues(alpha: 0.2)), + valueColor: AlwaysStoppedAnimation(colorScheme.primary.withValues(alpha: 0.2)), ), // Progress circle (indeterminate if no progress, determinate otherwise) CircularProgressIndicator( value: (progressPercent != null && progressPercent > 0) ? progressPercent : null, // null = indeterminate strokeWidth: 2.0, - valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.primary), + valueColor: AlwaysStoppedAnimation(colorScheme.primary), ), ], ), @@ -931,13 +932,10 @@ class _MediaDetailScreenState extends State /// Build a metadata chip with optional leading icon or widget Widget _buildMetadataChip(String text, {IconData? icon, Widget? leading}) { + final colorScheme = Theme.of(context).colorScheme; final textWidget = Text( text, - style: TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontSize: 13, - fontWeight: FontWeight.w500, - ), + style: TextStyle(color: colorScheme.onSecondaryContainer, fontSize: 13, fontWeight: FontWeight.w500), ); final hasLeading = leading != null || icon != null; @@ -945,7 +943,7 @@ class _MediaDetailScreenState extends State return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.8), + color: colorScheme.secondaryContainer.withValues(alpha: 0.8), borderRadius: const BorderRadius.all(Radius.circular(100)), ), child: hasLeading @@ -955,7 +953,7 @@ class _MediaDetailScreenState extends State if (leading != null) leading else - AppIcon(icon!, fill: 1, color: Theme.of(context).colorScheme.onSecondaryContainer, size: 16), + AppIcon(icon!, fill: 1, color: colorScheme.onSecondaryContainer, size: 16), const SizedBox(width: 4), textWidget, ], @@ -1095,16 +1093,13 @@ class _MediaDetailScreenState extends State /// Build a combined RT chip showing critic + audience side by side. Widget _buildCombinedRtChip(RatingInfo critic, RatingInfo audience) { - final textStyle = TextStyle( - color: Theme.of(context).colorScheme.onSecondaryContainer, - fontSize: 13, - fontWeight: FontWeight.w500, - ); + final colorScheme = Theme.of(context).colorScheme; + final textStyle = TextStyle(color: colorScheme.onSecondaryContainer, fontSize: 13, fontWeight: FontWeight.w500); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.8), + color: colorScheme.secondaryContainer.withValues(alpha: 0.8), borderRadius: const BorderRadius.all(Radius.circular(100)), ), child: Row( @@ -2397,6 +2392,7 @@ class _MediaDetailScreenState extends State final isShow = metadata.isShow; final isMobile = PlatformDetector.isMobile(context); final isTv = PlatformDetector.isTV(); + final theme = Theme.of(context); KeyEventResult handleBack(FocusNode _, KeyEvent event) => handleBackKeyNavigation(context, event, result: _watchStateChanged); @@ -2614,7 +2610,7 @@ class _MediaDetailScreenState extends State else Text( metadata.displayTitle, - style: Theme.of(context).textTheme.displaySmall?.copyWith( + style: theme.textTheme.displaySmall?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, shadows: [Shadow(color: Colors.black.withValues(alpha: 0.5), blurRadius: 8)], @@ -2662,7 +2658,7 @@ class _MediaDetailScreenState extends State Text( key: _overviewSectionKey, t.discover.overview, - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), Focus( @@ -2671,6 +2667,7 @@ class _MediaDetailScreenState extends State onFocusChange: (_) => setState(() {}), child: Builder( builder: (context) { + final innerTheme = Theme.of(context); final showFocus = _overviewFocusNode.hasFocus && InputModeTracker.isKeyboardMode(context); return AnimatedContainer( @@ -2680,13 +2677,13 @@ class _MediaDetailScreenState extends State borderRadius: const BorderRadius.all(Radius.circular(8)), border: Border.all( color: showFocus - ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.5) + ? innerTheme.colorScheme.primary.withValues(alpha: 0.5) : Colors.transparent, width: 2, ), ), child: () { - final summaryStyle = Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.6); + final summaryStyle = innerTheme.textTheme.bodyLarge?.copyWith(height: 1.6); if (isTv) { return Text(metadata.summary!, style: summaryStyle); } @@ -2716,7 +2713,7 @@ class _MediaDetailScreenState extends State child: Center( child: Text( t.messages.noSeasonsFound, - style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey), + style: theme.textTheme.bodyLarge?.copyWith(color: Colors.grey), ), ), ) @@ -2724,7 +2721,7 @@ class _MediaDetailScreenState extends State Text( key: _seasonsSectionKey, t.libraries.groupings.episodes, - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), _buildSeasonTabs(), @@ -2741,7 +2738,7 @@ class _MediaDetailScreenState extends State child: Center( child: Text( t.messages.noEpisodesFoundGeneral, - style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey), + style: theme.textTheme.bodyLarge?.copyWith(color: Colors.grey), ), ), ), @@ -2752,7 +2749,7 @@ class _MediaDetailScreenState extends State Text( key: _seasonsSectionKey, t.libraries.groupings.episodes, - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), if (_isLoadingSeasons || _isLoadingEpisodes) @@ -2767,7 +2764,7 @@ class _MediaDetailScreenState extends State child: Center( child: Text( t.messages.noEpisodesFoundGeneral, - style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.grey), + style: theme.textTheme.bodyLarge?.copyWith(color: Colors.grey), ), ), ), @@ -2779,7 +2776,7 @@ class _MediaDetailScreenState extends State Text( key: _castSectionKey, t.discover.cast, - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), _buildCastSection(metadata), @@ -2791,7 +2788,7 @@ class _MediaDetailScreenState extends State Text( key: _extrasSectionKey, t.discover.extras, - style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), _buildExtrasSection(), @@ -2860,9 +2857,9 @@ class _MediaDetailScreenState extends State begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - Theme.of(context).scaffoldBackgroundColor.withValues(alpha: 0.8), - Theme.of(context).scaffoldBackgroundColor.withValues(alpha: 0.5), - Theme.of(context).scaffoldBackgroundColor.withValues(alpha: 0), + theme.scaffoldBackgroundColor.withValues(alpha: 0.8), + theme.scaffoldBackgroundColor.withValues(alpha: 0.5), + theme.scaffoldBackgroundColor.withValues(alpha: 0), ], stops: const [0.0, 0.3, 1.0], ), @@ -2936,6 +2933,9 @@ class _MediaDetailScreenState extends State final containerHeight = imageSize + innerPadding * 2 + 66 + 16; final hasFocus = _castFocusNode.hasFocus; + final theme = Theme.of(context); + final actorNameStyle = theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600); + final actorRoleStyle = theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant); return Focus( focusNode: _castFocusNode, @@ -2986,19 +2986,12 @@ class _MediaDetailScreenState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - actor.tag, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), + Text(actor.tag, style: actorNameStyle, maxLines: 2, overflow: TextOverflow.ellipsis), if (actor.role != null) ...[ const SizedBox(height: 2), Text( actor.role!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + style: actorRoleStyle, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -3068,6 +3061,7 @@ class _MediaDetailScreenState extends State } Widget _buildInfoRow(String label, String value) { + final theme = Theme.of(context); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -3075,10 +3069,10 @@ class _MediaDetailScreenState extends State width: 120, child: Text( label, - style: TextStyle(fontWeight: FontWeight.w600, color: Theme.of(context).colorScheme.onSurfaceVariant), + style: TextStyle(fontWeight: FontWeight.w600, color: theme.colorScheme.onSurfaceVariant), ), ), - Expanded(child: Text(value, style: Theme.of(context).textTheme.bodyLarge)), + Expanded(child: Text(value, style: theme.textTheme.bodyLarge)), ], ); }