Completed: - SettingsService generic helpers (~15% reduction) - Removed unused providers (1,615 lines dead code) Deferred (risk outweighs benefit): - MusicAssistantProvider split - evolved with Sendspin, ID mapping - Library screen tab extraction - tight state coupling - API domain split - shared WebSocket connection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
11 KiB
Ensemble Codebase Audit Report
Date: 2026-01-14 Scope: Full codebase audit (104 Dart files, 18 screens) Focus Areas: UI Continuity, Performance, Logic/Best Practices
Executive Summary
The Ensemble codebase is a mature, feature-rich Flutter application with many good patterns already in place. However, the audit identified significant architectural debt that should be addressed:
| Severity | Count | Key Themes |
|---|---|---|
| Critical | 6 | God classes (5,443 & 4,209 lines), design token adoption, startup blocking |
| High | 15 | Touch targets, batch DB operations, over-notification, code duplication |
| Medium | 25+ | Inconsistencies, memory issues, missing error boundaries |
| Low | 15+ | Minor polish items |
Top 3 Priorities:
- Split
MusicAssistantProvider(5,443 lines) - God object handling everything - Split
new_library_screen.dart(4,209 lines) - 10 scroll controllers, 15+ view modes - Fix startup performance - Sequential blocking ops could save 1-8 seconds
Critical Issues
1. God Object: MusicAssistantProvider (5,443 lines)
File: lib/providers/music_assistant_provider.dart
- 54
notifyListeners()calls - 30+ state variables
- 100+ public methods
- Handles: connection, auth, players, library, search, caching, local playback, Sendspin
- Fix: Split into ConnectionManager, PlayerStateManager, LibraryStateManager, SearchStateManager
2. God Object: NewLibraryScreen (4,209 lines)
File: lib/screens/new_library_screen.dart
- 10 scroll controllers
- 15+ view mode states
- 15+ sort order states
- Handles: Music, Books, Podcasts, Radio tabs
- Fix: Extract to ArtistsTab, AlbumsTab, TracksTab, PlaylistsTab, BooksTab, PodcastsTab, RadioTab
3. Sequential Blocking Startup (500-1500ms wasted)
File: lib/main.dart:33-66
await DatabaseService.instance.initialize();
await ProfileService.instance.migrateFromOwnerName();
await SettingsService.migrateToSecureStorage();
await SyncService.instance.loadFromCache();
audioHandler = await AudioService.init(...);
- Fix: Parallelize with
Future.wait(), defer AudioService to post-first-frame
4. Design Tokens Exist But Unused
File: lib/theme/design_tokens.dart
Spacing.*used in only ~6 of 34+ widget filesRadii.*used in only ~5 filesIconSizes.*used in only ~4 files- Fix: Systematic replacement of hardcoded values
5. Touch Targets Too Small (32x32)
File: lib/screens/new_library_screen.dart:1805-1867
- Sort, favorites, view mode buttons are 32x32
- Material Design minimum: 48x48
- Fix: Increase to 48x48 or add padding for hit area
6. No Batch Database Inserts
File: lib/services/sync_service.dart:212-273
- 1000 albums = 1000 separate DB transactions
- Fix: Wrap in single transaction or use Drift batch insert
High Severity Issues
Performance
| Issue | File | Impact |
|---|---|---|
context.watch() in nested tabs |
new_library_screen.dart:2960 | All tabs rebuild on any provider change |
| Consumer wrapping 800-line player | expandable_player.dart:1234 | Full rebuild on every state change |
| Inline sorting during build | new_library_screen.dart:3647 | 5-20ms for 1000+ albums |
| Multiple setState for author images | new_library_screen.dart:1017 | 20+ rebuilds as images load |
| SecureStorage migration every startup | secure_storage_service.dart:103 | 100-300ms wasted |
Architecture
| Issue | File | Impact |
|---|---|---|
| State duplication across 4 providers | providers/*.dart | Maintenance burden, sync issues |
8 duplicate _cycle*ViewMode() methods |
new_library_screen.dart:251-402 | DRY violation |
| 60+ duplicate getter/setter pairs | settings_service.dart:500-1000 | Code bloat |
| Global singleton NavigationProvider | navigation_provider.dart:40 | Untestable |
| CacheService duplicated in providers | Multiple | Memory waste, inconsistency |
UI/UX
| Issue | File | Impact |
|---|---|---|
| Hardcoded bottom spacing (140, 164, 100) | Multiple detail screens | Content obscured by player |
| Inconsistent button sizes (44 vs 50) | Multiple screens | Visual disharmony |
| No debounce on card taps | album_card.dart:118 | Double navigation possible |
| Missing PopScope on AudiobookSeriesScreen | audiobook_series_screen.dart:276 | Adaptive colors not cleared |
Error Handling
| Issue | File | Impact |
|---|---|---|
| No Flutter error boundaries | main.dart | Red error screen on build failure |
| API returns empty list on error | music_assistant_api.dart | Can't distinguish error from empty |
Medium Severity Issues
Performance
- PaletteGenerator on main isolate (10-30ms blocking)
- Multiple
SharedPreferences.getInstance()calls (132 occurrences) - AppStartup polling loop with fixed 250ms delay
- SystemThemeHelper FutureBuilder recreated on every build
UI Consistency
- Border radius varies: 8, 12, 20, 22, 25 for same element types
SizedBox(height: 8)appears 40+ times instead ofSpacing.vGap8- Hardcoded colors for status (Colors.red, Colors.green)
- Inconsistent icon sizes across screens
State Management
- Over-notification (multiple notifyListeners in single methods)
- Wide Consumer scope (watch instead of select)
- Missing dispose() in some providers
- Inconsistent async state handling (loading/error)
Navigation
isQueuePanelOpenvsisQueuePanelTargetOpeninconsistencypopToHome()doesn't clear adaptive colors- Infinite navigation loop possible (Album → Artist → Album)
- Orphaned Hero animations in podcast_card, radio_station_card
Data/Caching
- Unbounded
_podcastCoverCache - SyncService listener not removed on error
- Redundant API calls between providers
- Fire-and-forget persistence race condition
Error Handling
- No zone error handler for async exceptions
retryCritical()retries all errors including non-recoverable- No user notification of offline queue status
- DisconnectedState not used consistently
Quick Wins (High Impact, Low Effort)
| # | Task | File(s) | Effort |
|---|---|---|---|
| 1 | Replace hardcoded bottom spacing with BottomSpacing.withMiniPlayer |
Detail screens | 30 min |
| 2 | Add navigation debounce to card widgets | album_card, artist_card, etc. | 1 hr |
| 3 | Fix touch targets to 48x48 minimum | new_library_screen.dart | 15 min |
| 4 | Add migration skip flag for SecureStorage | secure_storage_service.dart | 30 min |
| 5 | Add Flutter error boundary | main.dart | 30 min |
| 6 | Use context.select() instead of context.watch() |
global_player_overlay, etc. | 2 hr |
| 7 | Cache SharedPreferences.getInstance() |
settings_service.dart | 30 min |
Refactoring Roadmap
Phase 1: Quick Wins (1-2 days)
- Fix touch targets
- Fix bottom spacing
- Add navigation debounce
- Add error boundaries
- Cache SharedPreferences singleton
Phase 2: Performance (3-5 days)
- Parallelize startup operations
- Replace
context.watch()withcontext.select() - Batch database inserts
- Add SecureStorage migration skip flag
- Fix over-notification in provider
Phase 3: Design System (1 week)
- Systematic design token adoption
- Standardize button sizes (44 vs 50)
- Standardize border radius values
- Create StatusColors semantic tokens
- Replace all magic numbers
Phase 4: Architecture (2-3 weeks)
Split MusicAssistantProvider into focused managersDEFERRED - MusicAssistantProvider has evolved with Sendspin, Cast-Sendspin ID mapping, position tracker integration. Splitting would require ~1600 lines of sync work. Code is working and tested.Split new_library_screen into tab widgetsDEFERRED - Tabs have tight state coupling (15+ view modes, 15+ sort orders, 10 scroll controllers). Extraction would require InheritedWidget/callbacks for all shared state. Risk outweighs benefit.- ✅ Extract generic helpers for SettingsService - Reduced from 1,031 to 874 lines (~15%)
- ✅ Remove/integrate unused providers - Deleted PlayerProvider (964 lines), LibraryProvider, ConnectionProvider (651 lines total)
Split music_assistant_api.dart by domainDEFERRED - All methods share WebSocket connection and auth state. Clean extraction would require connection manager abstraction.
Positive Findings
The codebase already demonstrates many good practices:
- Performance awareness:
// PERF:comments, RepaintBoundary (55 occurrences), image caching (80 memCacheWidth/Height) - Caching: Multi-tier caching with LRU eviction, proper TTLs, proper DB indexes
- Error handling: DebugLogger, RetryHelper, OfflineActionQueue well-implemented
- Theme system: Material You support, adaptive colors from album art
- Offline support: Cache-first loading, graceful degradation
- Navigation: Consistent FadeSlidePageRoute, proper Hero animations (mostly)
Files by Priority
| Priority | File | Lines | Key Issues |
|---|---|---|---|
| 1 | music_assistant_provider.dart | 5,443 | God object, over-notification |
| 2 | new_library_screen.dart | 4,209 | God class, performance, touch targets |
| 3 | main.dart | 549 | Startup blocking, error boundaries |
| 4 | music_assistant_api.dart | 3,434 | Silent failures, needs splitting |
| 5 | settings_service.dart | 1,019 | Code duplication, no caching |
| 6 | expandable_player.dart | ~800 | Wide Consumer scope |
| 7 | global_player_overlay.dart | ~500 | BottomSpacing source, Consumer scope |
| 8 | All detail screens | Various | Hardcoded bottom spacing |
| 9 | All card widgets | Various | Navigation debounce, design tokens |
Audit Methodology
This audit was conducted using 9 parallel sub-agents analyzing:
UI Continuity (3 agents)
- Navigation & Flow - back button, Hero animations, tab preservation
- Visual Consistency - design tokens, spacing, typography
- Component Placement - touch targets, bottom spacing, modals
Performance (3 agents) 4. Rendering Performance - ListView.builder, const, rebuilds 5. Data & Caching - streams, invalidation, memory bounds 6. Startup & Load Time - blocking operations, initialization order
Logic/Best Practices (3 agents) 7. State Management - Provider patterns, notification frequency 8. Error Handling - try-catch, boundaries, resilience 9. Code Quality - file size, duplication, SOLID principles
Conclusion
The Ensemble app is well-architected at the macro level but has accumulated technical debt in its largest files. The most impactful improvements would be:
- Splitting the two 4,000+ line files into focused components
- Adopting design tokens systematically for visual consistency
- Optimizing startup sequence for faster app launch
- Using granular state management (select vs watch) for better performance
The quick wins section provides immediate value with minimal risk, while the roadmap offers a path to sustainable improvement.