flutter_spotify_remote

pub package pub points platforms License: MIT

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 SpotifyEvent exhaustively with switch
  • 🚨 Typed exceptions — SpotifyNotInstalledException, SpotifyAuthException, and friends
  • 📦 iOS framework bundled — SpotifyiOS.xcframework ships with the plugin; no extra Podfile source needed

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

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 initializeSession or a stored token.
  • The Android .aar must 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.

Libraries

flutter_spotify_remote
A Flutter plugin for Spotify App Remote SDK.