So here’s the story.
I was facing a technical debt kind of issue in my Flutter music app — my MusicPlaybackService was doing two jobs:
- Talking to the audio engine (
audio_service + just_audio) to play/pause/skip.
- Acting like a
ChangeNotifier to update the UI about song lists, current song, etc.
Why this was bad
At first, it felt convenient.
"Why not just let one class handle everything, right?"
But here’s the problem with one giant class doing multiple jobs:
- Unnecessary widget rebuilds: Even small playback state changes could cause the entire UI to refresh.
- Harder debugging: Was a bug coming from the UI or the audio engine? You never know without digging deep.
- Tight coupling: Any change in playback logic risked breaking the UI.
- Poor testability: Couldn’t mock or test the UI without pulling in the full audio player.
The Refactor
I split the responsibilities like this:
Before:
MusicPlaybackService
↳ Handles playback (play/pause/skip)
↳ Stores allSongs, currentSong
↳ Notifies UI of changes
After:
MusicPlaybackService → Only handles audio playback logic
PlaybackProvider → Holds UI-facing state & notifies widgets
How the new structure works
MusicPlaybackService → Talks to the background audio handler, keeps pure playback logic. No notifyListeners() here.
PlaybackProvider → Watches the service, stores the current song list, play state, etc., and only notifies the UI when something UI-relevant changes.
Benefits I noticed immediately
Fewer unnecessary rebuilds → smoother scrolling, faster tab switches.
Easier debugging → I instantly know whether an issue is in the audio engine or UI.
More modular code → I can swap playback logic without rewriting UI state.
Better testing → I can mock the provider without worrying about actual audio playback.
Lesson learned
If your service class is doing too much — step back and split the responsibilities.
It’s like having a drummer who’s also trying to do the vocals and mix the sound live. Eventually… something’s going to be off-beat.