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 ### Integration Status
✅ **Phase 1 Complete**: All 5 providers created ✅ **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 - All providers added to MultiProvider tree
- Shared services (CacheService, PositionTracker, ImageHelperService) properly initialized - 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 ### Remaining Work
+18 -20
View File
@@ -3420,13 +3420,12 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
Widget _buildPodcastsTab(BuildContext context, S l10n) { Widget _buildPodcastsTab(BuildContext context, S l10n) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; 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 // Use podcastsUnfiltered to avoid double-filtering with MA's provider filter
final (allPodcasts, isLoading, enabledProviders) = context.select<MusicAssistantProvider, (List<MediaItem>, bool, Set<String>)>( final maProvider = context.watch<MusicAssistantProvider>();
(p) => (p.podcastsUnfiltered, p.isLoadingPodcasts, p.enabledProviderIds.toSet()), final allPodcasts = maProvider.podcastsUnfiltered;
); final isLoading = maProvider.isLoadingPodcasts;
// Use read() for methods that don't need reactive updates final enabledProviders = maProvider.enabledProviderIds.toSet();
final maProvider = context.read<MusicAssistantProvider>();
if (isLoading) { if (isLoading) {
return Center(child: CircularProgressIndicator(color: colorScheme.primary)); return Center(child: CircularProgressIndicator(color: colorScheme.primary));
@@ -3724,13 +3723,12 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
Widget _buildRadioStationsTab(BuildContext context, S l10n) { Widget _buildRadioStationsTab(BuildContext context, S l10n) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; 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 // Use radioStationsUnfiltered to avoid double-filtering with MA's provider filter
final (allRadioStations, isLoading, enabledProviders) = context.select<MusicAssistantProvider, (List<MediaItem>, bool, Set<String>)>( final maProvider = context.watch<MusicAssistantProvider>();
(p) => (p.radioStationsUnfiltered, p.isLoadingRadio, p.enabledProviderIds.toSet()), final allRadioStations = maProvider.radioStationsUnfiltered;
); final isLoading = maProvider.isLoadingRadio;
// Use read() for methods that don't need reactive updates final enabledProviders = maProvider.enabledProviderIds.toSet();
final maProvider = context.read<MusicAssistantProvider>();
if (isLoading) { if (isLoading) {
return Center(child: CircularProgressIndicator(color: colorScheme.primary)); return Center(child: CircularProgressIndicator(color: colorScheme.primary));
@@ -3972,11 +3970,11 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
// ============ ARTISTS TAB ============ // ============ ARTISTS TAB ============
Widget _buildArtistsTab(BuildContext context, S l10n) { 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 // SyncService changes are handled by _onSyncServiceChanged listener
final (isLoading, enabledProviders) = context.select<MusicAssistantProvider, (bool, Set<String>)>( final provider = context.watch<MusicAssistantProvider>();
(p) => (p.isLoading, p.enabledProviderIds.toSet()), final isLoading = provider.isLoading;
); final enabledProviders = provider.enabledProviderIds.toSet();
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final syncService = SyncService.instance; final syncService = SyncService.instance;
@@ -4209,11 +4207,11 @@ class _NewLibraryScreenState extends State<NewLibraryScreen>
// ============ ALBUMS TAB ============ // ============ ALBUMS TAB ============
Widget _buildAlbumsTab(BuildContext context, S l10n) { 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 // SyncService changes are handled by _onSyncServiceChanged listener
final (isLoading, enabledProviders) = context.select<MusicAssistantProvider, (bool, Set<String>)>( final provider = context.watch<MusicAssistantProvider>();
(p) => (p.isLoading, p.enabledProviderIds.toSet()), final isLoading = provider.isLoading;
); final enabledProviders = provider.enabledProviderIds.toSet();
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final syncService = SyncService.instance; final syncService = SyncService.instance;
+2 -1
View File
@@ -106,6 +106,7 @@ class LibraryStatusService extends ChangeNotifier {
required bool Function(dynamic item) getFavorite, required bool Function(dynamic item) getFavorite,
required String Function(dynamic item) getProvider, required String Function(dynamic item) getProvider,
required String Function(dynamic item) getItemId, required String Function(dynamic item) getItemId,
bool notify = true,
}) { }) {
bool changed = false; bool changed = false;
@@ -130,7 +131,7 @@ class LibraryStatusService extends ChangeNotifier {
} }
} }
if (changed) { if (changed && notify) {
notifyListeners(); notifyListeners();
} }
} }
+20 -5
View File
@@ -417,6 +417,7 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
_controller.removeListener(_recordAnimationFrame); _controller.removeListener(_recordAnimationFrame);
_lyricsScrollController.removeListener(_onLyricsScroll); _lyricsScrollController.removeListener(_onLyricsScroll);
_expansionNotifyTimer?.cancel();
_controller.dispose(); _controller.dispose();
_queuePanelController.dispose(); _queuePanelController.dispose();
_sleepTimerPanelController.dispose(); _sleepTimerPanelController.dispose();
@@ -615,6 +616,8 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
// PERF Phase 4: Track last notified value to avoid unnecessary object creation // PERF Phase 4: Track last notified value to avoid unnecessary object creation
double _lastNotifiedProgress = -1; double _lastNotifiedProgress = -1;
Timer? _expansionNotifyTimer;
double? _pendingProgress;
void _notifyExpansionProgress() { void _notifyExpansionProgress() {
final progress = _controller.value; final progress = _controller.value;
@@ -623,11 +626,23 @@ class ExpandablePlayerState extends State<ExpandablePlayer>
if ((progress - _lastNotifiedProgress).abs() >= 0.01 || if ((progress - _lastNotifiedProgress).abs() >= 0.01 ||
progress == 0.0 || progress == 1.0) { progress == 0.0 || progress == 1.0) {
_lastNotifiedProgress = progress; _lastNotifiedProgress = progress;
playerExpansionNotifier.value = PlayerExpansionState( _pendingProgress = progress;
progress,
_currentExpandedBgColor, // Cancel any pending timer to avoid queue buildup
_currentExpandedPrimaryColor, _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(
_pendingProgress!,
_currentExpandedBgColor,
_currentExpandedPrimaryColor,
);
}
_expansionNotifyTimer = null;
_pendingProgress = null;
});
} }
} }
+13 -4
View File
@@ -109,6 +109,8 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
/// Whether the item is currently a favorite /// Whether the item is currently a favorite
bool get isFavorite => LibraryStatusService.instance.isFavorite(libraryItemKey); bool get isFavorite => LibraryStatusService.instance.isFavorite(libraryItemKey);
bool _isStatusUpdatePending = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -122,10 +124,17 @@ mixin LibraryStatusMixin<T extends StatefulWidget> on State<T> {
} }
void _onLibraryStatusChanged() { void _onLibraryStatusChanged() {
// Trigger rebuild to pick up new status values // Use addPostFrameCallback to ensure setState is called AFTER the build phase
if (mounted) { // This prevents "setState during build" errors
setState(() {}); if (_isStatusUpdatePending) return;
}
_isStatusUpdatePending = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {});
}
_isStatusUpdatePending = false;
});
} }
/// Set library status optimistically and return previous value for rollback /// Set library status optimistically and return previous value for rollback