flutter_spotify_remote 0.0.5
flutter_spotify_remote: ^0.0.5 copied to clipboard
Control the Spotify app from Flutter: playback commands, a real-time player state stream, and OAuth with silent token refresh on both iOS and Android.
flutter_spotify_remote #
Control the Spotify app from Flutter. flutter_spotify_remote wraps the official
Spotify App Remote SDK on both platforms and gives you one Dart API for
playback commands, a real-time player-state stream, and OAuth with silent token
refresh.
await FlutterSpotifyRemote.instance.play('spotify:track:6rqhFgbbKwnb9MLmUQDhG6');
Features #
- 🎛 Playback control — play, pause, resume, skip, seek, shuffle, repeat
- 🔊 Volume control — set, step, and mute, with slider-safe request coalescing (Premium)
- 📡 Real-time state — a
Stream<SpotifyEvent>of player state and connection changes - 🔐 OAuth with silent refresh — backend token swap/refresh on both iOS and Android, so users approve once and never app-switch again
- 🧩 Sealed event hierarchy — handle every
SpotifyEventexhaustively withswitch - 🚨 Typed exceptions —
SpotifyNotInstalledException,SpotifyAuthException, and friends - 📦 iOS framework bundled —
SpotifyiOS.xcframeworkships with the plugin; no extra Podfilesourceneeded
Platform support #
| Platform | Supported | Min version | Underlying SDK |
|---|---|---|---|
| Android | ✅ | API 21 | Spotify Android App Remote SDK (.aar) |
| iOS | ✅ | iOS 14 | SpotifyiOS 5.0.1 (xcframework, bundled) |
The Spotify app must be installed and logged in on the device — this plugin remote-controls it, it does not stream audio itself.
Installation #
dependencies:
flutter_spotify_remote: ^0.0.4
Then register your app in the
Spotify Developer Dashboard to get a
client ID, and add your redirect URI (e.g. yourapp://callback) there.
iOS setup #
1. Install pods #
The SpotifyiOS.xcframework is vendored by the plugin's podspec, so a plain
pod install is all that is required:
cd ios && pod install
2. Register the redirect URI scheme #
In ios/Runner/Info.plist, declare the scheme half of your redirect URI and allow querying the Spotify app:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>spotify</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>
The redirect callback is forwarded automatically through Flutter's plugin
registrar — no AppDelegate changes are needed.
Android setup #
The Spotify Android App Remote SDK .aar cannot be redistributed via Maven and
must be added to your app manually.
1. Download the SDK #
Grab spotify-app-remote-release-*.aar from the
Spotify Android SDK releases.
2. Place the .aar in your app #
android/
app/
libs/
spotify-app-remote-release-0.8.0.aar
3. Reference it in android/app/build.gradle(.kts) #
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
}
4. Add the internet permission #
<uses-permission android:name="android.permission.INTERNET" />
5. Declare the redirect URI placeholders #
Authorization runs through the Spotify auth library, which registers a redirect
receiver activity built from two manifest placeholders. The manifest merge
fails without them. Set both to match your redirectUrl — for
yourapp://callback that is scheme yourapp, host callback:
android {
defaultConfig {
manifestPlaceholders["redirectSchemeName"] = "yourapp"
manifestPlaceholders["redirectHostName"] = "callback"
}
}
Package visibility (<queries> for com.spotify.music, required on Android 11+)
is contributed by the plugin, so no host-app entry is needed.
Quick start #
import 'package:flutter_spotify_remote/flutter_spotify_remote.dart';
final spotify = FlutterSpotifyRemote.instance;
// 1. Listen for events *before* connecting
spotify.onEvent.listen((event) {
switch (event) {
case SpotifyPlayerStateChangedEvent(:final playerState):
print('Now playing: ${playerState.track.name}');
case SpotifyConnectionChangedEvent(:final status):
print('Connection: $status');
case SpotifyAccessTokenEvent(:final accessToken, :final expiresIn):
prefs.setString('spotify_token', accessToken); // persist it
}
});
// 2. Authorize (see "Authentication" below for the production path)
await spotify.connectAndAuthorize(
clientId: 'YOUR_CLIENT_ID',
redirectUrl: 'yourapp://callback',
);
// 3. Control playback
await spotify.play('spotify:track:6rqhFgbbKwnb9MLmUQDhG6');
await spotify.pause();
await spotify.resume();
await spotify.skipNext();
await spotify.skipPrevious();
await spotify.seekTo(30000); // 30 seconds
await spotify.setShuffle(true);
await spotify.setRepeatMode(1); // 0=off 1=track 2=context
// Volume — Spotify Premium only, see the section below
await spotify.setVolume(40); // 0-100
await spotify.volumeUp(step: 10);
await spotify.toggleMute();
final level = await spotify.getVolume();
await spotify.disconnect();
A complete runnable app lives in example/.
Authentication #
Recommended: initializeSession with a backend #
Call initializeSession() on every app start with your token swap and refresh
endpoints. The native SDK then performs first-time OAuth, and refreshes the
token silently (~every 55 minutes) with no further app-switch:
await spotify.initializeSession(
clientId: 'YOUR_CLIENT_ID',
redirectUrl: 'yourapp://callback',
tokenSwapURL: 'https://your-backend.example.com/swap',
tokenRefreshURL: 'https://your-backend.example.com/refresh',
);
// Later, to force a refresh:
await spotify.renewSession();
Both endpoints take a form-encoded POST and return standard Spotify JSON
(access_token, expires_in, optional refresh_token):
| Endpoint | Request body |
|---|---|
tokenSwapURL |
grant_type=authorization_code&code=…&redirect_uri=… |
tokenRefreshURL |
grant_type=refresh_token&refresh_token=… |
Your client secret stays on the backend — never ship it in the app.
Without a tokenSwapURL, Android falls back to the implicit grant:
initializeSession still emits an access token, but there is no refresh token,
so renewSession() fails with NO_SESSION.
Silent reconnect on later launches #
Persist the token from SpotifyAccessTokenEvent and reconnect without any
app-switch:
await spotify.connectWithToken(
clientId: 'YOUR_CLIENT_ID',
redirectUrl: 'yourapp://callback',
accessToken: storedToken,
);
Tokens are never stored natively — persistence is yours to control, in
SharedPreferences or secure storage:
case SpotifyAccessTokenEvent(:final accessToken, :final expiresIn, :final source):
await prefs.setString('spotify_token', accessToken);
await prefs.setInt(
'spotify_token_expires',
DateTime.now().millisecondsSinceEpoch + expiresIn * 1000,
);
source tells you how the token arrived: firstAuth, silentRefresh, or
directAuth.
API reference #
FlutterSpotifyRemote.instance #
| Method | Description |
|---|---|
initializeSession(...) |
Configure OAuth with backend swap/refresh (recommended) |
renewSession() |
Force a silent token refresh |
connectAndAuthorize(...) |
OAuth app-switch without a backend |
connectWithToken(...) |
Silent connect using a stored token |
disconnect() |
Disconnect and release SDK resources |
isConnected() |
Whether the App Remote is connected |
getAccessToken() |
The current access token |
canPlayOnDemand() |
Whether the account may play an individual track URI |
play(uri) |
Play a Spotify URI |
pause() / resume() |
Pause / resume playback |
skipNext() / skipPrevious() |
Track navigation |
seekTo(ms) |
Seek within the current track |
setShuffle(bool) |
Enable/disable shuffle |
setRepeatMode(int) |
0=off, 1=track, 2=context |
setVolume(percent) |
Set volume 0–100 (clamped) — Premium, see Volume |
getVolume() |
Read the active device's volume as 0–100 |
volumeUp({step}) / volumeDown({step}) |
Step the volume, default 10 points |
mute() / unmute() / toggleMute() |
Mute, remembering the previous level |
isMuted |
Whether mute() is active (no network call) |
lastKnownVolume |
Last volume set or read, or null (no network call) |
setAccessToken(token) |
Supply the token the volume methods use — Android |
onEvent |
Stream<SpotifyEvent> of all events |
Events #
| Event | Fields |
|---|---|
SpotifyPlayerStateChangedEvent |
playerState (SpotifyPlayerState) |
SpotifyConnectionChangedEvent |
status, message? |
SpotifyAccessTokenEvent |
accessToken, expiresIn, source |
Exceptions #
| Exception | When thrown |
|---|---|
SpotifyNotInstalledException |
Spotify is not installed |
SpotifyAuthException |
Token expired or auth failed |
SpotifyConnectionException |
Connection attempt failed |
SpotifyCommandException |
Playback command rejected by the SDK |
SpotifyPremiumRequiredException |
Volume change rejected — account is not Premium |
SpotifyNoActiveDeviceException |
No active Spotify device to set the volume on |
SpotifyWebApiException |
Other Web API failure (statusCode, message) |
Volume #
The Spotify App Remote SDK has no volume API — not on iOS, not on Android.
So setVolume and friends go through the Spotify Web API
(PUT /me/player/volume) against your account's active device. The plugin
handles the request, the token, and the error mapping; you just call it.
await spotify.setVolume(40); // 0-100, clamped
final level = await spotify.getVolume(); // 0-100
await spotify.volumeUp(); // +10 points, capped at 100
await spotify.volumeDown(step: 5); // -5 points, floored at 0
await spotify.mute(); // remembers the current level
await spotify.unmute(); // restores it
await spotify.toggleMute();
Requirements #
| Spotify Premium | Free accounts get SpotifyPremiumRequiredException (HTTP 403) |
| Scopes | user-modify-playback-state and user-read-playback-state — both are in the default scopes of initializeSession |
| An active device | Start playback first, or pass deviceId:. Otherwise SpotifyNoActiveDeviceException |
| A valid token | Cached automatically from connectWithToken and SpotifyAccessTokenEvent. If you restore a token from storage without connecting, call setAccessToken first |
Driving a slider #
setVolume coalesces rapid calls — one request is in flight at a time and
intermediate values are dropped, so the last value always wins without
tripping Spotify's rate limit. Call it straight from onChanged:
Slider(
value: _volume,
onChanged: (v) {
setState(() => _volume = v);
spotify.setVolume((v * 100).round()); // safe per-frame
},
)
Use lastKnownVolume and isMuted to render UI without a network round-trip;
they are updated by every successful call.
Handling failures #
try {
await spotify.setVolume(60);
} on SpotifyPremiumRequiredException {
// Free account — hide the volume UI
} on SpotifyNoActiveDeviceException {
// Nothing is playing yet
} on SpotifyAuthException {
await spotify.renewSession();
}
Known limitations #
- Spotify must be installed and logged in; this plugin does not stream audio.
- Spotify Free accounts cannot play a single track URI on demand — check
canPlayOnDemand()and fall back to a playlist or album context. - The first authorization always app-switches to Spotify. Every later launch
is silent if you use
initializeSessionor a stored token. - The Android
.aarmust be added manually — Spotify's terms prevent distribution through Maven Central. - Volume requires Premium and a network round-trip. The App Remote SDK exposes no volume API on either platform, so the plugin uses the Spotify Web API — which is Premium-only and needs an active device. It changes Spotify's volume, not the device's system media volume.
Contributing #
Issues and pull requests are welcome on
GitHub. Please run
flutter analyze and flutter test before opening a PR.
License #
MIT — see LICENSE.