Skip to content

Commit

Permalink
fix/Spotify: App crashes when there's no internet connection
Browse files Browse the repository at this point in the history
- Added a loading state for the Spotify track search.
- Updated the Spotify search service to handle pagination.
- Added error handling for Spotify track search.

Signed-off-by: Gabriel Fontán <[email protected]>
  • Loading branch information
BobbyESP committed Aug 15, 2024
1 parent 11d623e commit 2be24e3
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 75 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class SpotifyServiceImpl @Inject constructor(
if (api == null) {
buildApi()
}
return api ?: throw IllegalStateException("Spotify API is null")
return api
?: throw IllegalStateException("The connection to the Spotify API was not established. This may be due to a network error or a servers outage")
}

override suspend fun getSpotifyToken(): Token {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.bobbyesp.metadator.features.spotify.data.remote.search

import androidx.paging.Pager
import androidx.paging.PagingConfig
import com.adamratzman.spotify.endpoints.pub.SearchApi
import com.adamratzman.spotify.models.SearchFilter
import com.adamratzman.spotify.models.SpotifySearchResult
import com.adamratzman.spotify.models.Track
import com.bobbyesp.metadator.features.spotify.domain.pagination.TracksPagingSource
import com.bobbyesp.metadator.features.spotify.domain.services.SpotifyService
import com.bobbyesp.metadator.features.spotify.domain.services.search.SpotifySearchService
import javax.inject.Inject
Expand All @@ -18,4 +22,24 @@ class SpotifySearchServiceImpl @Inject constructor(
val api = spotifyService.getSpotifyApi()
return api.search.search(query = query, searchTypes = searchTypes, filters = filters)
}

override suspend fun searchPaginatedTracks(
query: String,
filters: List<SearchFilter>
): Pager<Int, Track> {
val api = spotifyService.getSpotifyApi()
return Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 40,
),
pagingSourceFactory = {
TracksPagingSource(
spotifyApi = api,
query = query,
)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@ class SearchRepositoryImpl @Inject constructor(
* does not support it. For now, it will only return up to 50 results.
*/
override suspend fun searchTracks(query: String): Result<List<Track>> {
return try {
try {
val searchResult = searchService.search(
query,
searchTypes = arrayOf(SearchApi.SearchType.Track),
filters = listOf()
filters = emptyList()
)
searchResult.let {
if (it.tracks == null) return Result.failure(IllegalStateException("Search result is null"))
}
Result.success(searchResult.tracks!!.items)
} catch (e: Exception) {
Result.failure(e)

searchResult.tracks?.let { return Result.success(it.items) }
?: return Result.failure(NullPointerException("Search result is null"))
} catch (th: Throwable) {
return Result.failure(th)
}
}

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package com.bobbyesp.metadator.features.spotify.domain.services.search

import androidx.paging.Pager
import com.adamratzman.spotify.endpoints.pub.SearchApi
import com.adamratzman.spotify.models.SearchFilter
import com.adamratzman.spotify.models.SpotifySearchResult
import com.adamratzman.spotify.models.Track

interface SpotifySearchService {
suspend fun search(
query: String,
vararg searchTypes: SearchApi.SearchType,
filters: List<SearchFilter>
): SpotifySearchResult

suspend fun searchPaginatedTracks(
query: String,
filters: List<SearchFilter>,
): Pager<Int, Track>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,26 @@ package com.bobbyesp.metadator.presentation.pages.utilities.tageditor.spotify

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.adamratzman.spotify.SpotifyAppApi
import com.adamratzman.spotify.models.Track
import com.bobbyesp.metadator.features.spotify.domain.pagination.TracksPagingSource
import com.bobbyesp.metadator.features.spotify.domain.services.SpotifyService
import com.bobbyesp.metadator.features.spotify.domain.services.search.SpotifySearchService
import com.bobbyesp.utilities.states.ResourceState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class MetadataBsVM @Inject constructor(
private val spotifyService: SpotifyService
private val searchService: SpotifySearchService
) : ViewModel() {
private lateinit var spotifyApi: SpotifyAppApi

init {
viewModelScope.launch {
spotifyApi = spotifyService.getSpotifyApi()
}
}

private val mutableViewStateFlow = MutableStateFlow(ViewState())
val viewStateFlow = mutableViewStateFlow.asStateFlow()

Expand All @@ -42,7 +30,7 @@ class MetadataBsVM @Inject constructor(

data class ViewState(
val stage: BottomSheetStage = BottomSheetStage.SEARCH,
val searchedTracks: Flow<PagingData<Track>> = emptyFlow(),
val searchedTracks: ResourceState<Flow<PagingData<Track>>> = ResourceState.Loading(null),
val selectedTrack: Track? = null,
val lastQuery: String = "",
)
Expand All @@ -54,25 +42,26 @@ class MetadataBsVM @Inject constructor(
}
}

private fun getTracksPaginatedData(query: String) {
val tracksPager = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 40,
),
pagingSourceFactory = {
TracksPagingSource(
spotifyApi = spotifyApi,
query = query,
private suspend fun getTracksPaginatedData(query: String) {
try {
val tracksPager = searchService.searchPaginatedTracks(
query = query,
filters = emptyList()
).flow.cachedIn(viewModelScope)

mutableViewStateFlow.update {
it.copy(
searchedTracks = ResourceState.Success(tracksPager)
)
}
} catch (th: Throwable) {
mutableViewStateFlow.update {
it.copy(
searchedTracks = ResourceState.Error(
message = th.message ?: th.stackTrace.toString()
)
)
}
).flow.cachedIn(viewModelScope)

mutableViewStateFlow.update {
it.copy(
searchedTracks = tracksPager
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.bobbyesp.metadator.presentation.pages.utilities.tageditor.spotify.stages

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
Expand All @@ -11,8 +12,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
Expand All @@ -31,6 +32,8 @@ import com.adamratzman.spotify.models.Track
import com.bobbyesp.metadator.R
import com.bobbyesp.metadator.presentation.components.cards.songs.spotify.SpotifyHorizontalSongCard
import com.bobbyesp.metadator.presentation.pages.utilities.tageditor.spotify.MetadataBsVM
import com.bobbyesp.ui.components.state.LoadingState
import com.bobbyesp.utilities.states.ResourceState
import com.bobbyesp.utilities.ui.pagingStateHandler

@OptIn(ExperimentalFoundationApi::class)
Expand All @@ -42,7 +45,8 @@ fun SpMetadataBsSearch(
pageViewState: State<MetadataBsVM.ViewState>,
onChooseTrack: (Track) -> Unit
) {
val paginatedTracks = pageViewState.value.searchedTracks.collectAsLazyPagingItems()
val paginatedTracksState = pageViewState.value.searchedTracks
val paginatedTracks = paginatedTracksState.data?.collectAsLazyPagingItems()

LazyColumn(
state = listState,
Expand Down Expand Up @@ -80,37 +84,67 @@ fun SpMetadataBsSearch(
}
}

items(
count = paginatedTracks.itemCount,
key = paginatedTracks.itemKey(),
contentType = paginatedTracks.itemContentType()
) { index ->
val item = paginatedTracks[index] ?: return@items
SpotifyHorizontalSongCard(
innerModifier = Modifier.padding(8.dp),
surfaceColor = Color.Transparent,
track = item,
onClick = {
onChooseTrack(item)
when (paginatedTracksState) {
is ResourceState.Loading -> {
item {
LoadingState(stringResource(id = R.string.retrieving_spotify_token))
}
}

is ResourceState.Success -> {
items(
count = paginatedTracks!!.itemCount,
key = paginatedTracks.itemKey(),
contentType = paginatedTracks.itemContentType()
) { index ->
val item = paginatedTracks[index] ?: return@items
SpotifyHorizontalSongCard(
innerModifier = Modifier.padding(8.dp),
surfaceColor = Color.Transparent,
track = item,
onClick = {
onChooseTrack(item)
}
)
}

pagingStateHandler(paginatedTracks, itemCount = 1) {
LoadingState(stringResource(id = R.string.loading))
}
}

is ResourceState.Error -> {
item {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
border = BorderStroke(
1.dp,
MaterialTheme.colorScheme.error.copy(alpha = 0.5f)
),
contentColor = MaterialTheme.colorScheme.error,
shape = MaterialTheme.shapes.medium
) {
Column(
modifier = Modifier.padding(4.dp),
verticalArrangement = Arrangement.spacedBy(
8.dp,
Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
modifier = Modifier,
text = paginatedTracksState.message
?: stringResource(id = com.bobbyesp.ui.R.string.unknown_error_title),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Normal
)
}
}
}
)
}
pagingStateHandler(paginatedTracks, itemCount = 1) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.loading),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth()
)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(0.8f)
)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@
<string name="marquee_text">Marquee text</string>
<string name="marquee_text_description">The text that overflows the limits of the screen has an animation to show its entire content (this may lag the app).</string>
<string name="lyrics">Lyrics</string>
<string name="retrieving_spotify_token">Retrieveing Spotify API token...</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.bobbyesp.ui.components.state

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp

@Composable
fun LoadingState(text: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(
8.dp,
Alignment.CenterVertically
),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
modifier = Modifier.fillMaxWidth()
)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(0.8f)
)
}
}

0 comments on commit 2be24e3

Please sign in to comment.