Skip to content

Commit

Permalink
fix: Resolve albums pagination bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
PolinaPolupan committed Dec 24, 2024
1 parent 1ed2729 commit ca536e5
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 22 deletions.
40 changes: 40 additions & 0 deletions .idea/androidTestResultsUserPreferences.xml

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

18 changes: 16 additions & 2 deletions .idea/deploymentTargetSelector.xml

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

5 changes: 5 additions & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.paging.room)
implementation(libs.network.response.adapter)
androidTestImplementation(project(":core:designsystem"))
compileOnly(libs.androidx.hilt.common)
implementation(libs.hilt.android)
implementation(libs.androidx.room.ktx)
Expand All @@ -59,5 +60,9 @@ dependencies {

androidTestImplementation(libs.androidx.junit.test.ext)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.hilt.android)
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(project(":core:testing"))

kspAndroidTest(libs.hilt.compiler)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.example.mymusic.core.data

import android.content.Context
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingConfig
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.mymusic.core.data.mediator.AlbumsRemoteMediator
import com.example.mymusic.core.database.MusicDatabase
import com.example.mymusic.core.database.model.LocalAlbumWithArtists
import com.example.mymusic.core.database.model.entities.LocalAlbum
import com.example.mymusic.core.designsystem.component.PreviewParameterData
import com.example.mymusic.core.network.fake.FakeNetworkDataSource
import com.example.mymusic.core.network.model.SavedAlbum
import com.example.mymusic.core.network.model.SavedAlbumsResponse
import com.example.mymusic.core.network.model.toLocal
import com.example.mymusic.core.network.model.toLocalAlbum
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

@RunWith(AndroidJUnit4::class)
class AlbumsRemoteMediatorTest {

private lateinit var mockAlbums: List<LocalAlbumWithArtists>

private val testDispatcher = StandardTestDispatcher()

private lateinit var mockApi: FakeNetworkDataSource

private val context: Context = ApplicationProvider.getApplicationContext()

private val mockDb = Room.inMemoryDatabaseBuilder(
context,
MusicDatabase::class.java
).build()

@Before
fun setUp() {
mockApi= FakeNetworkDataSource(
dispatcher = testDispatcher,
context = ApplicationProvider.getApplicationContext()
)
}


@After
fun tearDown() {
mockDb.clearAllTables()
}

@OptIn(ExperimentalPagingApi::class)
@Test
fun refreshLoadReturnsSuccessResultWhenMoreDataIsPresent() = runTest(testDispatcher, timeout = 1000.seconds) {

mockAlbums = mockApi.getSavedAlbums(0, 5)?.items?.map {
LocalAlbumWithArtists(
album = LocalAlbum(
id = it.album.id,
type = it.album.type,
imageUrl = it.album.images[0].url,
name = it.album.name
),
simplifiedArtists = it.album.artists.toLocal()
)
} ?: emptyList()

val remoteMediator = AlbumsRemoteMediator(
networkDataSource = mockApi,
musicDatabase = mockDb
)

val pagingState = PagingState<Int, LocalAlbumWithArtists>(
listOf(),
null,
PagingConfig(5),
10
)

val result = remoteMediator.load(LoadType.REFRESH, pagingState)
assertTrue(result is RemoteMediator.MediatorResult.Success)
assertFalse((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
}

@OptIn(ExperimentalPagingApi::class)
@Test
fun refreshLoadSuccessAndEndOfPaginationWhenNoMoreData() = runTest(testDispatcher, timeout = 1000.seconds) {
// To test endOfPaginationReached, don't set up the mockApi to return post
// data here.
val remoteMediator = AlbumsRemoteMediator(
networkDataSource = mockApi,
musicDatabase = mockDb
)
val pagingState = PagingState<Int, LocalAlbumWithArtists>(
listOf(),
null,
PagingConfig(0),
10
)

val result = remoteMediator.load(LoadType.REFRESH, pagingState)
assertTrue(result is RemoteMediator.MediatorResult.Success)
assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class AlbumsRemoteMediator @Inject constructor(
val next = data?.next
val albums = data?.items

val endOfPaginationReached = albums?.isEmpty() == true || next == null
val endOfPaginationReached = albums?.isEmpty() == true

musicDatabase.withTransaction {

Expand All @@ -78,7 +78,7 @@ class AlbumsRemoteMediator @Inject constructor(
musicDatabase.remoteKeysDao().insertAllKeys(keys)
}

if (albums != null) {
if (!albums.isNullOrEmpty()) {

musicDao.upsertSavedAlbums(albums.toLocal())
musicDao.upsertAlbums(albums.toLocalAlbum())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,40 @@ class FakeNetworkDataSource @Inject constructor(
val inputStream = context.resources.openRawResource(R.raw.saved_albums)
.bufferedReader().use { it.readText() }

Json.decodeFromString<SavedAlbumsResponse?>(inputStream)
val response = Json.decodeFromString<SavedAlbumsResponse?>(inputStream)
val albums = response?.items

val albumsPaginated = albums?.drop(offset)?.take(limit)
SavedAlbumsResponse(
href = response?.href ?: "",
limit = limit,
next = response?.next,
offset = offset,
previous = response?.previous,
total = response?.total ?: 0,
items = albumsPaginated ?: emptyList()
)
}

override suspend fun getSavedPlaylists(offset: Int, limit: Int): SavedPlaylistResponse? =
withContext(dispatcher) {
val inputStream = context.resources.openRawResource(R.raw.saved_playlists)
.bufferedReader().use { it.readText() }

Json.decodeFromString<SavedPlaylistResponse?>(inputStream)
val response = Json.decodeFromString<SavedPlaylistResponse?>(inputStream)
val playlists = response?.items

val playlistsPaginated = playlists?.drop(offset)?.take(limit)

SavedPlaylistResponse(
href = response?.href ?: "",
limit = limit,
next = response?.next,
offset = offset,
previous = response?.previous,
total = response?.total ?: 0,
items = playlistsPaginated ?: emptyList()
)
}

override suspend fun getPlaylistTracks(id: String, fields: String): List<PlaylistTrack> =
Expand Down
1 change: 1 addition & 0 deletions feature/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dependencies {
androidTestImplementation(libs.androidx.junit.test.ext)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.paging.testing)
androidTestImplementation(project(":core:testing"))
kspAndroidTest(libs.hilt.compiler)
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.example.mymusic.feature.home.AuthenticatedUiState
import com.example.mymusic.core.designsystem.component.AlbumCard
import com.example.mymusic.core.designsystem.component.AnimationBox
Expand All @@ -59,6 +61,8 @@ import com.example.mymusic.core.designsystem.component.SortBottomSheet
import com.example.mymusic.core.designsystem.component.SortOption
import com.example.mymusic.core.designsystem.theme.MyMusicTheme
import com.example.mymusic.core.designsystem.util.lerpScrollOffset
import com.example.mymusic.core.model.SimplifiedAlbum
import com.example.mymusic.core.model.SimplifiedPlaylist
import kotlinx.coroutines.flow.flowOf
import kotlin.math.abs

Expand Down Expand Up @@ -116,8 +120,8 @@ fun LibraryScreen(
@Composable
fun LibraryContent(
uiState: AuthenticatedUiState,
albums: LazyPagingItems<com.example.mymusic.core.model.SimplifiedAlbum>,
playlists: LazyPagingItems<com.example.mymusic.core.model.SimplifiedPlaylist>,
albums: LazyPagingItems<SimplifiedAlbum>,
playlists: LazyPagingItems<SimplifiedPlaylist>,
onSortOptionChanged: (SortOption) -> Unit,
onNavigateToPlaylist: (String) -> Unit,
onNavigateToAlbumClick: (String) -> Unit,
Expand Down Expand Up @@ -233,40 +237,43 @@ fun LibraryContent(
}

fun LazyListScope.albumsList(
albums: LazyPagingItems<com.example.mymusic.core.model.SimplifiedAlbum>,
albums: LazyPagingItems<SimplifiedAlbum>,
onNavigateToAlbumClick: (String) -> Unit,
onAlbumClick: (String) -> Unit
) {
items(
items = albums.itemSnapshotList,
key = { it?.id ?: "0" }
) {
album ->
AnimationBox {
key = albums.itemKey { it.id },
count = albums.itemCount,
contentType = albums.itemContentType { "albums" }
) { index ->
val album = albums[index]

AlbumCard(
name = album!!.name,
artists = album.artists,
imageUrl = album.imageUrl,
onClick = {
onAlbumClick(album.id)
onNavigateToAlbumClick(album.id) },
onNavigateToAlbumClick(album.id)
},
modifier = Modifier
.fillMaxWidth()
)
}

}
}

fun LazyListScope.playlistsList(
playlists: LazyPagingItems<com.example.mymusic.core.model.SimplifiedPlaylist>,
playlists: LazyPagingItems<SimplifiedPlaylist>,
onNavigateToPlaylistClick: (String) -> Unit,
onPlaylistClick: (String) -> Unit
) {
items(
items = playlists.itemSnapshotList,
key = { it?.id ?: "0" }
) { playlist ->
AnimationBox {
key = playlists.itemKey { it.id },
count = playlists.itemCount,
contentType = playlists.itemContentType { "playlists" }
) { index ->
val playlist = playlists[index]
PlaylistCard(
name = playlist!!.name,
ownerName = playlist.ownerName,
Expand All @@ -279,7 +286,7 @@ fun LazyListScope.playlistsList(
},
modifier = Modifier.fillMaxWidth()
)
}

}
}

Expand Down

0 comments on commit ca536e5

Please sign in to comment.