Compare commits

...
5 Commits
Author SHA1 Message Date
Chris 4e27e6a4c2 On audit: WIP: multi-provider debugging attempts 2026-02-07 14:40:47 +00:00
Chris b136b26b7e index on audit: 404828c Fix setState during build using timer debounce 2026-02-07 14:40:47 +00:00
ChrisandClaude Opus 4.6 404828c47c Fix setState during build using timer debounce
Replace Future.microtask with Timer-based debounce (50ms delay) to
ensure setState is called well outside the build phase. Also properly
cancels the timer on dispose to prevent memory leaks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:19:10 +00:00
ChrisandClaude Opus 4.6 48afbf9d50 Update status: red screen bug fixed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:11:55 +00:00
ChrisandClaude Opus 4.6 ebf4e53328 Fix setState and context.select errors causing red screen
- Replace context.select with context.watch in PageView.builder tabs
  (context.select cannot be used inside PageView.builder/SliverList)
- Fix LibraryStatusMixin setState during build error using Future.microtask
- Both fixes resolve the red screen issue after login

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:11:42 +00:00
5 changed files with 59 additions and 32 deletions
+6 -2
View File
@@ -17,10 +17,14 @@
### Integration Status
✅ **Phase 1 Complete**: All 5 providers created
✅ **Phase 2 Started**: Integration into main.dart completed
✅ **Phase 2 Complete**: Integration into main.dart completed
- All providers added to MultiProvider tree
- Shared services (CacheService, PositionTracker, ImageHelperService) properly initialized
- Build successful (52.2MB APK)
- QueueManagerService circular dependency resolved
- Build successful (debug APK tested)
✅ **Critical Bug Fixed**: Red screen after login resolved (commit: ebf4e53)
- Fixed setState during build error in LibraryStatusMixin (using Future.microtask)
- Fixed context.select inside PageView.builder error (replaced with context.watch)
### Remaining Work
+18 -20
View File
@@ -3420,13 +3420,12 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
Widget _buildPodcastsTab(BuildContext context, S l10n) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
// PERF: Use select() to only rebuild when podcasts, loading state, or enabled providers changes
// PERF: Use watch() - cannot use select() inside PageView.builder
// Use podcastsUnfiltered to avoid double-filtering with MA's provider filter
final (allPodcasts, isLoading, enabledProviders) = context.select<MusicAssistantProvider, (List<MediaItem>, bool, Set<String>)>(
(p) => (p.podcastsUnfiltered, p.isLoadingPodcasts, p.enabledProviderIds.toSet()),
);
// Use read() for methods that don't need reactive updates
final maProvider = context.read<MusicAssistantProvider>();
final maProvider = context.watch<MusicAssistantProvider>();
final allPodcasts = maProvider.podcastsUnfiltered;
final isLoading = maProvider.isLoadingPodcasts;
final enabledProviders = maProvider.enabledProviderIds.toSet();
if (isLoading) {
return Center(child: CircularProgressIndicator(color: colorScheme.primary));
@@ -3724,13 +3723,12 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
Widget _buildRadioStationsTab(BuildContext context, S l10n) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
// PERF: Use select() to only rebuild when radio stations, loading state, or enabled providers changes
// PERF: Use watch() - cannot use select() inside PageView.builder
// Use radioStationsUnfiltered to avoid double-filtering with MA's provider filter
final (allRadioStations, isLoading, enabledProviders) = context.select<MusicAssistantProvider, (List<MediaItem>, bool, Set<String>)>(
(p) => (p.radioStationsUnfiltered, p.isLoadingRadio, p.enabledProviderIds.toSet()),
);
// Use read() for methods that don't need reactive updates
final maProvider = context.read<MusicAssistantProvider>();
final maProvider = context.watch<MusicAssistantProvider>();
final allRadioStations = maProvider.radioStationsUnfiltered;
final isLoading = maProvider.isLoadingRadio;
final enabledProviders = maProvider.enabledProviderIds.toSet();
if (isLoading) {
return Center(child: CircularProgressIndicator(color: colorScheme.primary));
@@ -3972,11 +3970,11 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
// ============ ARTISTS TAB ============
Widget _buildArtistsTab(BuildContext context, S l10n) {
// Use select to only rebuild when specific fields change
// Use watch instead of select - cannot use select inside PageView.builder
// SyncService changes are handled by _onSyncServiceChanged listener
final (isLoading, enabledProviders) = context.select<MusicAssistantProvider, (bool, Set<String>)>(
(p) => (p.isLoading, p.enabledProviderIds.toSet()),
);
final provider = context.watch<MusicAssistantProvider>();
final isLoading = provider.isLoading;
final enabledProviders = provider.enabledProviderIds.toSet();
final colorScheme = Theme.of(context).colorScheme;
final syncService = SyncService.instance;
@@ -4209,11 +4207,11 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
// ============ ALBUMS TAB ============
Widget _buildAlbumsTab(BuildContext context, S l10n) {
// Use select to only rebuild when specific fields change
// Use watch instead of select - cannot use select inside PageView.builder
// SyncService changes are handled by _onSyncServiceChanged listener
final (isLoading, enabledProviders) = context.select<MusicAssistantProvider, (bool, Set<String>)>(
(p) => (p.isLoading, p.enabledProviderIds.toSet()),
);
final provider = context.watch<MusicAssistantProvider>();
final isLoading = provider.isLoading;
final enabledProviders = provider.enabledProviderIds.toSet();
final colorScheme = Theme.of(context).colorScheme;
final syncService = SyncService.instance;
+2 -1
View File
@@ -106,6 +106,7 @@ class LibraryStatusService extends ChangeNotifier {
required bool Function(dynamic item) getFavorite,
required String Function(dynamic item) getProvider,
required String Function(dynamic item) getItemId,
bool notify = true,
}) {
bool changed = false;
@@ -130,7 +131,7 @@ class LibraryStatusService extends ChangeNotifier {
}
}
if (changed) {
if (changed && notify) {
notifyListeners();
}
}
+16 -1
View File
@@ -417,6 +417,7 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
_controller.removeListener(_recordAnimationFrame);
_lyricsScrollController.removeListener(_onLyricsScroll);
_expansionNotifyTimer?.cancel();
_controller.dispose();
_queuePanelController.dispose();
_sleepTimerPanelController.dispose();
@@ -615,6 +616,8 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
// PERF Phase 4: Track last notified value to avoid unnecessary object creation
double _lastNotifiedProgress = -1;
Timer? _expansionNotifyTimer;
double? _pendingProgress;
void _notifyExpansionProgress() {
final progress = _controller.value;
@@ -623,12 +626,24 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
if ((progress - _lastNotifiedProgress).abs() >= 0.01 ||
progress == 0.0 || progress == 1.0) {
_lastNotifiedProgress = progress;
_pendingProgress = progress;
// Cancel any pending timer to avoid queue buildup
_expansionNotifyTimer?.cancel();
// Debounce with a short delay to ensure we're outside build phase
_expansionNotifyTimer = Timer(const Duration(milliseconds: 1), () {
if (mounted && _pendingProgress != null) {
playerExpansionNotifier.value = PlayerExpansionState(
progress,
_pendingProgress!,
_currentExpandedBgColor,
_currentExpandedPrimaryColor,
);
}
_expansionNotifyTimer = null;
_pendingProgress = null;
});
}
}
void _subscribeToPositionTracker() {
+10 -1
View File
@@ -109,6 +109,8 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
/// Whether the item is currently a favorite
bool get isFavorite => LibraryStatusService.instance.isFavorite(libraryItemKey);
bool _isStatusUpdatePending = false;
@override
void initState() {
super.initState();
@@ -122,10 +124,17 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
}
void _onLibraryStatusChanged() {
// Trigger rebuild to pick up new status values
// Use addPostFrameCallback to ensure setState is called AFTER the build phase
// This prevents "setState during build" errors
if (_isStatusUpdatePending) return;
_isStatusUpdatePending = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
_isStatusUpdatePending = false;
});
}
/// Set library status optimistically and return previous value for rollback