Integrating Spotify in Android Apps: Web API + SDK Tutorial 2025
Olisemeka Nwaeme
July 5, 2025 · 9 min read min read
0
0

1<!-- Example: If your app is called "MusicPlayer" -->
2musicplayer://callback
3
1<activity
2 android:name=".MainActivity"
3 android:exported="true"
4 android:launchMode="singleTop">
5
6 <!-- Your existing intent filters -->
7 <intent-filter>
8 <action android:name="android.intent.action.MAIN" />
9 <category android:name="android.intent.category.LAUNCHER" />
10 </intent-filter>
11
12 <!-- Spotify authentication callback -->
13 <intent-filter>
14 <action android:name="android.intent.action.VIEW" />
15 <category android:name="android.intent.category.DEFAULT" />
16 <category android:name="android.intent.category.BROWSABLE" />
17 <data
18 android:host="callback"
19 android:scheme="musicplayer" />
20 </intent-filter>
21</activity>
22
1object SpotifyConstants {
2 const val CLIENT_ID = "your_spotify_client_id_here"
3 const val REDIRECT_URI = "musicplayer://callback"
4 const val REQUEST_CODE = 1337
5
6 // Spotify scopes - adjust based on your needs
7 val SCOPES = arrayOf(
8 "user-read-private",
9 "user-read-email",
10 "user-read-playback-state",
11 "user-modify-playback-state",
12 "user-read-currently-playing",
13 "playlist-read-private",
14 "playlist-read-collaborative",
15 "playlist-modify-public",
16 "playlist-modify-private"
17 )
18}
19
1dependencies {
2 implementation 'com.spotify.android:auth:2.1.0'
3 implementation files('libs/spotify-app-remote-release-0.8.0.aar')
4
5 // Additional dependencies for networking
6 implementation 'com.squareup.retrofit2:retrofit:2.9.0'
7 implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
8 implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
9}
10
1class SpotifyAuthManager(private val activity: Activity) {
2
3 fun authenticate() {
4 val builder = AuthorizationRequest.Builder(
5 SpotifyConstants.CLIENT_ID,
6 AuthorizationResponse.Type.TOKEN,
7 SpotifyConstants.REDIRECT_URI
8 )
9
10 builder.setScopes(SpotifyConstants.SCOPES)
11 builder.setShowDialog(false)
12
13 val request = builder.build()
14 AuthorizationClient.openLoginActivity(
15 activity,
16 SpotifyConstants.REQUEST_CODE,
17 request
18 )
19 }
20}
21
1class MainActivity : AppCompatActivity() {
2 private lateinit var spotifyAuthManager: SpotifyAuthManager
3 private var accessToken: String? = null
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContentView(R.layout.activity_main)
8
9 spotifyAuthManager = SpotifyAuthManager(this)
10
11 // Start authentication
12 spotifyAuthManager.authenticate()
13 }
14
15 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
16 super.onActivityResult(requestCode, resultCode, data)
17
18 if (requestCode == SpotifyConstants.REQUEST_CODE) {
19 val response = AuthorizationClient.getResponse(resultCode, data)
20
21 when (response.type) {
22 AuthorizationResponse.Type.TOKEN -> {
23 accessToken = response.accessToken
24 onAuthenticationComplete(response.accessToken)
25 }
26
27 AuthorizationResponse.Type.ERROR -> {
28 Log.e("SpotifyAuth", "Auth error: ${response.error}")
29 showError("Authentication failed: ${response.error}")
30 }
31
32 else -> {
33 Log.d("SpotifyAuth", "Auth cancelled")
34 }
35 }
36 }
37 }
38
39 private fun onAuthenticationComplete(token: String) {
40 // Token received successfully
41 Toast.makeText(this, "Authentication successful!", Toast.LENGTH_SHORT).show()
42
43 // Now you can use the Web API
44 setupSpotifyWebAPI(token)
45
46 // And connect to the App Remote
47 connectToSpotifyAppRemote()
48 }
49
50 private fun showError(message: String) {
51 Toast.makeText(this, message, Toast.LENGTH_LONG).show()
52 }
53}
54
1interface SpotifyWebAPI {
2 @GET("me")
3 suspend fun getCurrentUser(
4 @Header("Authorization") authorization: String
5 ): Response<User>
6
7 @GET("me/playlists")
8 suspend fun getUserPlaylists(
9 @Header("Authorization") authorization: String,
10 @Query("limit") limit: Int = 20
11 ): Response<PlaylistResponse>
12
13 @GET("me/player/currently-playing")
14 suspend fun getCurrentlyPlaying(
15 @Header("Authorization") authorization: String
16 ): Response<CurrentlyPlaying>
17
18 @GET("search")
19 suspend fun searchTracks(
20 @Header("Authorization") authorization: String,
21 @Query("q") query: String,
22 @Query("type") type: String = "track",
23 @Query("limit") limit: Int = 20
24 ): Response<SearchResponse>
25}
26
1data class User(
2 val id: String,
3 val display_name: String?,
4 val email: String?,
5 val images: List<Image>?
6)
7
8data class PlaylistResponse(
9 val items: List<Playlist>,
10 val total: Int
11)
12
13data class Playlist(
14 val id: String,
15 val name: String,
16 val description: String?,
17 val uri: String,
18 val images: List<Image>?
19)
20
21data class Image(
22 val url: String,
23 val height: Int?,
24 val width: Int?
25)
26
1class SpotifyApiClient {
2 companion object {
3 private const val BASE_URL = "https://api.spotify.com/v1/"
4
5 fun create(): SpotifyWebAPI {
6 val logging = HttpLoggingInterceptor().apply {
7 level = HttpLoggingInterceptor.Level.BODY
8 }
9
10 val client = OkHttpClient.Builder()
11 .addInterceptor(logging)
12 .build()
13
14 return Retrofit.Builder()
15 .baseUrl(BASE_URL)
16 .client(client)
17 .addConverterFactory(GsonConverterFactory.create())
18 .build()
19 .create(SpotifyWebAPI::class.java)
20 }
21 }
22}
23
1private fun setupSpotifyWebAPI(accessToken: String) {
2 val api = SpotifyApiClient.create()
3
4 lifecycleScope.launch {
5 try {
6 val authHeader = "Bearer $accessToken"
7
8 // Get current user
9 val userResponse = api.getCurrentUser(authHeader)
10 if (userResponse.isSuccessful) {
11 val user = userResponse.body()
12 Log.d("SpotifyAPI", "Welcome ${user?.display_name}")
13 }
14
15 // Get user's playlists
16 val playlistsResponse = api.getUserPlaylists(authHeader)
17 if (playlistsResponse.isSuccessful) {
18 val playlists = playlistsResponse.body()?.items
19 Log.d("SpotifyAPI", "Found ${playlists?.size} playlists")
20 }
21
22 } catch (e: Exception) {
23 Log.e("SpotifyAPI", "API Error: ${e.message}")
24 }
25 }
26}
27
1class SpotifyPlaybackManager(private val context: Context) {
2 private var spotifyAppRemote: SpotifyAppRemote? = null
3 private val connectionParams = ConnectionParams.Builder(SpotifyConstants.CLIENT_ID)
4 .setRedirectUri(SpotifyConstants.REDIRECT_URI)
5 .showAuthView(true)
6 .build()
7
8 fun connect(callback: (Boolean) -> Unit) {
9 SpotifyAppRemote.connect(
10 context,
11 connectionParams,
12 object : Connector.ConnectionListener {
13 override fun onConnected(appRemote: SpotifyAppRemote) {
14 spotifyAppRemote = appRemote
15 Log.d("SpotifyRemote", "Connected successfully")
16 callback(true)
17 }
18
19 override fun onFailure(throwable: Throwable) {
20 Log.e("SpotifyRemote", "Connection failed", throwable)
21 callback(false)
22 }
23 }
24 )
25 }
26
27 fun disconnect() {
28 spotifyAppRemote?.let {
29 SpotifyAppRemote.disconnect(it)
30 }
31 }
32
33 fun play(uri: String) {
34 spotifyAppRemote?.playerApi?.play(uri)
35 }
36
37 fun pause() {
38 spotifyAppRemote?.playerApi?.pause()
39 }
40
41 fun resume() {
42 spotifyAppRemote?.playerApi?.resume()
43 }
44
45 fun skipToNext() {
46 spotifyAppRemote?.playerApi?.skipNext()
47 }
48
49 fun skipToPrevious() {
50 spotifyAppRemote?.playerApi?.skipPrevious()
51 }
52
53 fun getCurrentPlayerState(callback: (PlayerState?) -> Unit) {
54 spotifyAppRemote?.playerApi?.playerState?.setResultCallback { playerState ->
55 callback(playerState)
56 }
57 }
58}
59
1private fun connectToSpotifyAppRemote() {
2 val playbackManager = SpotifyPlaybackManager(this)
3
4 playbackManager.connect { success ->
5 if (success) {
6 // Connection successful - you can now control playback
7 setupPlaybackControls(playbackManager)
8 } else {
9 showError("Failed to connect to Spotify app. Please ensure Spotify is installed.")
10 }
11 }
12}
13
14private fun setupPlaybackControls(playbackManager: SpotifyPlaybackManager) {
15 findViewById<Button>(R.id.btn_play).setOnClickListener {
16 // Play a specific track or playlist
17 playbackManager.play("spotify:track:4iV5W9uYEdYUVa79Axb7Rh")
18 }
19
20 findViewById<Button>(R.id.btn_pause).setOnClickListener {
21 playbackManager.pause()
22 }
23
24 findViewById<Button>(R.id.btn_next).setOnClickListener {
25 playbackManager.skipToNext()
26 }
27
28 // Get current player state
29 playbackManager.getCurrentPlayerState { playerState ->
30 playerState?.let { state ->
31 val isPlaying = !state.isPaused
32 val currentTrack = state.track
33 Log.d("PlayerState", "Playing: $isPlaying, Track: ${currentTrack.name}")
34 }
35 }
36}
37
1// Example retry implementation
2private suspend fun <T> apiCallWithRetry(
3 maxRetries: Int = 3,
4 apiCall: suspend () -> Response<T>
5): Response<T> {
6 repeat(maxRetries) { attempt ->
7 try {
8 val response = apiCall()
9 if (response.code() != 429) return response
10
11 // Wait before retry (exponential backoff)
12 delay(1000L * (attempt + 1))
13 } catch (e: Exception) {
14 if (attempt == maxRetries - 1) throw e
15 }
16 }
17 throw Exception("Max retries exceeded")
18}
19
1// Example project structure
2app/
3├── data/
4│ ├── api/
5│ │ ├── SpotifyWebAPI.kt
6│ │ ├── SpotifyApiClient.kt
7│ │ └── models/
8│ └── repository/
9│ └── SpotifyRepository.kt
10├── domain/
11│ ├── models/
12│ └── usecases/
13├── presentation/
14│ ├── viewmodels/
15│ ├── activities/
16│ └── fragments/
17└── utils/
18 ├── SpotifyConstants.kt
19 └── Extensions.kt
20
1fun subscribeToPlayerState(callback: (PlayerState) -> Unit) {
2 spotifyAppRemote?.playerApi?.subscribeToPlayerState()?.setEventCallback { playerState ->
3 callback(playerState)
4 }
5}
6
1suspend fun createPlaylist(
2 accessToken: String,
3 userId: String,
4 name: String,
5 description: String
6): Response<Playlist> {
7 val body = mapOf(
8 "name" to name,
9 "description" to description,
10 "public" to false
11 )
12
13 return api.createPlaylist("Bearer $accessToken", userId, body)
14}
15
1suspend fun searchWithFilters(
2 accessToken: String,
3 query: String,
4 type: String = "track,album,artist",
5 market: String = "US",
6 limit: Int = 20
7): Response<SearchResponse> {
8 return api.search(
9 authorization = "Bearer $accessToken",
10 q = query,
11 type = type,
12 market = market,
13 limit = limit
14 )
15}
16
Olise is a product-oriented Software Engineer specializing in Android development. As the Android Development Lead at Tanta Innovative, he’s dedicated to creating impactful mobile solutions that elevate client success and drive business efficiency. Beyond coding, Olise is a passionate Arsenal FC fan and anime lover. He values community and enjoys connecting with fellow developers at meetups.
Related Articles
Discover more insights and stories from our collection of articles

Getting started with the LLAMA 3.2-11B with groq
The LLAMA 3.2-11B model is a very powerful model that can perform both text and vision tasks.

Streamline Your Backend Workflow with Reusable Components
Learn how backend developers can save time by creating reusable components for common tasks, streamlining project setups, improving consistency, and boosting productivity.

Getting Started with Google Maps in Flutter
Learn how to integrate Google Maps into your Flutter app with this step-by-step guide. From API key setup to your first interactive map, get your mapping features running in minutes!