Implement change list invalidation in repositories

Change-Id: I75a1d378089a52eaf84f2fa7b01e54144d69107b
pull/2/head
Adetunji Dahunsi 3 years ago committed by Don Turner
parent 0d6dd0b079
commit a3ddf12aa6

@ -25,6 +25,13 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retry
data class ChangeListVersions(
val topicVersion: Int = -1,
val authorVersion: Int = -1,
val episodeVersion: Int = -1,
val newsResourceVersion: Int = -1,
)
class NiaPreferences @Inject constructor(
private val userPreferences: DataStore<UserPreferences>
) {
@ -81,4 +88,42 @@ class NiaPreferences @Inject constructor(
Log.e("NiaPreferences", "Failed to update user preferences", ioException)
}
}
suspend fun getChangeListVersions() = userPreferences.data
.map {
ChangeListVersions(
topicVersion = it.topicChangeListVersion,
authorVersion = it.authorChangeListVersion,
episodeVersion = it.episodeChangeListVersion,
newsResourceVersion = it.newsResourceChangeListVersion,
)
}
.firstOrNull() ?: ChangeListVersions()
/**
* Update the [ChangeListVersions] using [update].
*/
suspend fun updateChangeListVersion(update: ChangeListVersions.() -> ChangeListVersions) {
try {
userPreferences.updateData { currentPreferences ->
val updatedChangeListVersions = update(
ChangeListVersions(
topicVersion = currentPreferences.topicChangeListVersion,
authorVersion = currentPreferences.authorChangeListVersion,
episodeVersion = currentPreferences.episodeChangeListVersion,
newsResourceVersion = currentPreferences.newsResourceChangeListVersion
)
)
currentPreferences.copy {
topicChangeListVersion = updatedChangeListVersions.topicVersion
authorChangeListVersion = updatedChangeListVersions.authorVersion
episodeChangeListVersion = updatedChangeListVersions.episodeVersion
newsResourceChangeListVersion = updatedChangeListVersions.newsResourceVersion
}
}
} catch (ioException: IOException) {
Log.e("NiaPreferences", "Failed to update user preferences", ioException)
}
}
}

@ -22,4 +22,8 @@ option java_multiple_files = true;
message UserPreferences {
repeated int32 followed_topic_ids = 1;
bool has_run_first_time_sync = 2;
int32 topicChangeListVersion = 3;
int32 authorChangeListVersion = 4;
int32 episodeChangeListVersion = 5;
int32 newsResourceChangeListVersion = 6;
}

@ -17,6 +17,9 @@
package com.google.samples.apps.nowinandroid.core.domain
import android.util.Log
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import kotlin.coroutines.cancellation.CancellationException
/**
@ -35,3 +38,36 @@ suspend fun <T> suspendRunCatching(block: suspend () -> T): Result<T> = try {
)
Result.failure(exception)
}
/**
* Utility function for syncing a repository with the network.
* [versionReader] Reads the current version of the model that needs to be synced
* [changeListFetcher] Fetches the change list for the model
* [versionUpdater] Updates the [ChangeListVersions] after a successful sync
* [modelUpdater] Updates the model by consuming the ids of the models that have changed.
*
* Note that the blocks defined above may be run concurrently due to concurrent calls
* to [changeListSync]. It is the caller's responsibility to make [modelUpdater] performs atomic
* operations.
*/
suspend fun changeListSync(
niaPreferences: NiaPreferences,
versionReader: (ChangeListVersions) -> Int,
changeListFetcher: suspend (Int) -> List<NetworkChangeList>,
versionUpdater: ChangeListVersions.(Int) -> ChangeListVersions,
modelUpdater: suspend (List<Int>) -> Unit
) = suspendRunCatching {
// Fetch the change list since last sync (akin to a git fetch)
val currentVersion = versionReader(niaPreferences.getChangeListVersions())
val changeList = changeListFetcher(currentVersion)
if (changeList.isEmpty()) return@suspendRunCatching true
// Using the change list, pull down and save the changes (akin to a git pull)
modelUpdater(changeList.map(NetworkChangeList::id))
// Update the last synced version (akin to updating local git HEAD)
val latestVersion = changeList.last().changeListVersion
niaPreferences.updateChangeListVersion {
versionUpdater(latestVersion)
}
}.isSuccess

@ -19,8 +19,10 @@ package com.google.samples.apps.nowinandroid.core.domain.repository
import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.changeListSync
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.suspendRunCatching
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
@ -34,17 +36,27 @@ import kotlinx.coroutines.flow.map
class LocalAuthorsRepository @Inject constructor(
private val authorDao: AuthorDao,
private val network: NiANetwork,
private val niaPreferences: NiaPreferences,
) : AuthorsRepository {
override fun getAuthorsStream(): Flow<List<Author>> =
authorDao.getAuthorEntitiesStream()
.map { it.map(AuthorEntity::asExternalModel) }
// TODO: Pass change list for incremental sync. See b/227206738
override suspend fun sync(): Boolean = suspendRunCatching {
val networkAuthors = network.getAuthors()
authorDao.upsertAuthors(
entities = networkAuthors.map(NetworkAuthor::asEntity)
)
}.isSuccess
override suspend fun sync(): Boolean = changeListSync(
niaPreferences = niaPreferences,
versionReader = ChangeListVersions::authorVersion,
changeListFetcher = { currentVersion ->
network.getAuthorChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(authorVersion = latestVersion)
},
modelUpdater = { changedIds ->
val networkAuthors = network.getAuthors(ids = changedIds)
authorDao.upsertAuthors(
entities = networkAuthors.map(NetworkAuthor::asEntity)
)
}
)
}

@ -25,13 +25,15 @@ import com.google.samples.apps.nowinandroid.core.database.model.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.changeListSync
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.model.authorCrossReferences
import com.google.samples.apps.nowinandroid.core.domain.model.authorEntityShells
import com.google.samples.apps.nowinandroid.core.domain.model.episodeEntityShell
import com.google.samples.apps.nowinandroid.core.domain.model.topicCrossReferences
import com.google.samples.apps.nowinandroid.core.domain.model.topicEntityShells
import com.google.samples.apps.nowinandroid.core.domain.suspendRunCatching
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
@ -48,6 +50,7 @@ class LocalNewsRepository @Inject constructor(
private val authorDao: AuthorDao,
private val topicDao: TopicDao,
private val network: NiANetwork,
private val niaPreferences: NiaPreferences,
) : NewsRepository {
override fun getNewsResourcesStream(): Flow<List<NewsResource>> =
@ -58,44 +61,53 @@ class LocalNewsRepository @Inject constructor(
newsResourceDao.getNewsResourcesStream(filterTopicIds = filterTopicIds)
.map { it.map(PopulatedNewsResource::asExternalModel) }
// TODO: Pass change list for incremental sync. See b/227206738
override suspend fun sync() = suspendRunCatching {
val networkNewsResources = network.getNewsResources()
override suspend fun sync() = changeListSync(
niaPreferences = niaPreferences,
versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion ->
network.getNewsResourceChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(newsResourceVersion = latestVersion)
},
modelUpdater = { changedIds ->
val networkNewsResources = network.getNewsResources(ids = changedIds)
// Order of invocation matters to satisfy id and foreign key constraints!
// Order of invocation matters to satisfy id and foreign key constraints!
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
authorDao.insertOrIgnoreAuthors(
authorEntities = networkNewsResources
.map(NetworkNewsResource::authorEntityShells)
.flatten()
.distinctBy(AuthorEntity::id)
)
episodeDao.insertOrIgnoreEpisodes(
episodeEntities = networkNewsResources
.map(NetworkNewsResource::episodeEntityShell)
.distinctBy(EpisodeEntity::id)
)
newsResourceDao.upsertNewsResources(
newsResourceEntities = networkNewsResources
.map(NetworkNewsResource::asEntity)
)
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences = networkNewsResources
.map(NetworkNewsResource::topicCrossReferences)
.distinct()
.flatten()
)
newsResourceDao.insertOrIgnoreAuthorCrossRefEntities(
newsResourceAuthorCrossReferences = networkNewsResources
.map(NetworkNewsResource::authorCrossReferences)
.distinct()
.flatten()
)
}.isSuccess
topicDao.insertOrIgnoreTopics(
topicEntities = networkNewsResources
.map(NetworkNewsResource::topicEntityShells)
.flatten()
.distinctBy(TopicEntity::id)
)
authorDao.insertOrIgnoreAuthors(
authorEntities = networkNewsResources
.map(NetworkNewsResource::authorEntityShells)
.flatten()
.distinctBy(AuthorEntity::id)
)
episodeDao.insertOrIgnoreEpisodes(
episodeEntities = networkNewsResources
.map(NetworkNewsResource::episodeEntityShell)
.distinctBy(EpisodeEntity::id)
)
newsResourceDao.upsertNewsResources(
newsResourceEntities = networkNewsResources
.map(NetworkNewsResource::asEntity)
)
newsResourceDao.insertOrIgnoreTopicCrossRefEntities(
newsResourceTopicCrossReferences = networkNewsResources
.map(NetworkNewsResource::topicCrossReferences)
.distinct()
.flatten()
)
newsResourceDao.insertOrIgnoreAuthorCrossRefEntities(
newsResourceAuthorCrossReferences = networkNewsResources
.map(NetworkNewsResource::authorCrossReferences)
.distinct()
.flatten()
)
}
)
}

@ -19,9 +19,10 @@ package com.google.samples.apps.nowinandroid.core.domain.repository
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.domain.changeListSync
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.suspendRunCatching
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
@ -35,7 +36,7 @@ import kotlinx.coroutines.flow.map
class LocalTopicsRepository @Inject constructor(
private val topicDao: TopicDao,
private val network: NiANetwork,
private val niaPreferences: NiaPreferences
private val niaPreferences: NiaPreferences,
) : TopicsRepository {
override fun getTopicsStream(): Flow<List<Topic>> =
@ -53,11 +54,20 @@ class LocalTopicsRepository @Inject constructor(
override fun getFollowedTopicIdsStream() = niaPreferences.followedTopicIds
// TODO: Pass change list for incremental sync. See b/227206738
override suspend fun sync(): Boolean = suspendRunCatching {
val networkTopics = network.getTopics()
topicDao.upsertTopics(
entities = networkTopics.map(NetworkTopic::asEntity)
)
}.isSuccess
override suspend fun sync(): Boolean = changeListSync(
niaPreferences = niaPreferences,
versionReader = ChangeListVersions::topicVersion,
changeListFetcher = { currentVersion ->
network.getTopicChangeList(after = currentVersion)
},
versionUpdater = { latestVersion ->
copy(topicVersion = latestVersion)
},
modelUpdater = { changedIds ->
val networkTopics = network.getTopics(ids = changedIds)
topicDao.upsertTopics(
entities = networkTopics.map(NetworkTopic::asEntity)
)
}
)
}

@ -19,16 +19,19 @@ package com.google.samples.apps.nowinandroid.core.domain.repository
import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestAuthorDao
import com.google.samples.apps.nowinandroid.core.domain.testdoubles.TestNiaNetwork
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class LocalAuthorsRepositoryTest {
@ -36,16 +39,25 @@ class LocalAuthorsRepositoryTest {
private lateinit var authorDao: AuthorDao
private lateinit var network: NiANetwork
private lateinit var network: TestNiaNetwork
private lateinit var niaPreferences: NiaPreferences
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before
fun setup() {
authorDao = TestAuthorDao()
network = TestNiaNetwork()
niaPreferences = NiaPreferences(
tmpFolder.testUserPreferencesDataStore()
)
subject = LocalAuthorsRepository(
authorDao = authorDao,
network = network,
niaPreferences = niaPreferences,
)
}
@ -76,5 +88,41 @@ class LocalAuthorsRepositoryTest {
network.map(AuthorEntity::id),
db.map(AuthorEntity::id)
)
// After sync version should be updated
Assert.assertEquals(
network.lastIndex,
niaPreferences.getChangeListVersions().authorVersion
)
}
@Test
fun localAuthorsRepository_incremental_sync_pulls_from_network() =
runTest {
// Set author version to 5
niaPreferences.updateChangeListVersion {
copy(authorVersion = 5)
}
subject.sync()
val network = network.getAuthors()
.map(NetworkAuthor::asEntity)
// Drop 5 to simulate the first 5 items being unchanged
.drop(5)
val db = authorDao.getAuthorEntitiesStream()
.first()
Assert.assertEquals(
network.map(AuthorEntity::id),
db.map(AuthorEntity::id)
)
// After sync version should be updated
Assert.assertEquals(
network.lastIndex + 5,
niaPreferences.getChangeListVersions().authorVersion
)
}
}

@ -23,6 +23,8 @@ import com.google.samples.apps.nowinandroid.core.database.model.PopulatedEpisode
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
import com.google.samples.apps.nowinandroid.core.domain.model.asEntity
import com.google.samples.apps.nowinandroid.core.domain.model.authorCrossReferences
import com.google.samples.apps.nowinandroid.core.domain.model.authorEntityShells
@ -42,7 +44,9 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class LocalNewsRepositoryTest {
@ -58,6 +62,11 @@ class LocalNewsRepositoryTest {
private lateinit var network: TestNiaNetwork
private lateinit var niaPreferences: NiaPreferences
@get:Rule
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
@Before
fun setup() {
newsResourceDao = TestNewsResourceDao()
@ -65,6 +74,9 @@ class LocalNewsRepositoryTest {
authorDao = TestAuthorDao()
topicDao = TestTopicDao()
network = TestNiaNetwork()
niaPreferences = NiaPreferences(
tmpFolder.testUserPreferencesDataStore()
)
subject = LocalNewsRepository(
newsResourceDao = newsResourceDao,
@ -72,6 +84,7 @@ class LocalNewsRepositoryTest {
authorDao = authorDao,
topicDao = topicDao,
network = network,
niaPreferences = niaPreferences,
)
}
@ -122,6 +135,44 @@ class LocalNewsRepositoryTest {
newsResourcesFromNetwork.map(NewsResource::id),
newsResourcesFromDb.map(NewsResource::id)
)
// After sync version should be updated
assertEquals(
newsResourcesFromNetwork.lastIndex,
niaPreferences.getChangeListVersions().newsResourceVersion
)
}
@Test
fun localNewsRepository_incremental_sync_pulls_from_network() =
runTest {
// Set news version to 7
niaPreferences.updateChangeListVersion {
copy(newsResourceVersion = 7)
}
subject.sync()
val newsResourcesFromNetwork = network.getNewsResources()
.map(NetworkNewsResource::asEntity)
.map(NewsResourceEntity::asExternalModel)
// Drop 7 to simulate the first 7 items being unchanged
.drop(7)
val newsResourcesFromDb = newsResourceDao.getNewsResourcesStream()
.first()
.map(PopulatedNewsResource::asExternalModel)
assertEquals(
newsResourcesFromNetwork.map(NewsResource::id),
newsResourcesFromDb.map(NewsResource::id)
)
// After sync version should be updated
assertEquals(
newsResourcesFromNetwork.lastIndex + 7,
niaPreferences.getChangeListVersions().newsResourceVersion
)
}
@Test

@ -100,6 +100,42 @@ class LocalTopicsRepositoryTest {
network.map(TopicEntity::id),
db.map(TopicEntity::id)
)
// After sync version should be updated
Assert.assertEquals(
network.lastIndex,
niaPreferences.getChangeListVersions().topicVersion
)
}
@Test
fun localTopicsRepository_incremental_sync_pulls_from_network() =
runTest {
// Set topics version to 10
niaPreferences.updateChangeListVersion {
copy(topicVersion = 10)
}
subject.sync()
val network = network.getTopics()
.map(NetworkTopic::asEntity)
// Drop 10 to simulate the first 10 items being unchanged
.drop(10)
val db = topicDao.getTopicEntitiesStream()
.first()
Assert.assertEquals(
network.map(TopicEntity::id),
db.map(TopicEntity::id)
)
// After sync version should be updated
Assert.assertEquals(
network.lastIndex + 10,
niaPreferences.getChangeListVersions().topicVersion
)
}
@Test

@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.core.domain.testdoubles
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.fake.FakeDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.serialization.decodeFromString
@ -31,12 +32,68 @@ class TestNiaNetwork : NiANetwork {
private val networkJson = Json
override suspend fun getTopics(itemsPerPage: Int): List<NetworkTopic> =
networkJson.decodeFromString(FakeDataSource.topicsData)
override suspend fun getTopics(ids: List<Int>?): List<NetworkTopic> =
networkJson.decodeFromString<List<NetworkTopic>>(FakeDataSource.topicsData)
.matchIds(
ids = ids,
idGetter = NetworkTopic::id
)
override suspend fun getAuthors(itemsPerPage: Int): List<NetworkAuthor> =
networkJson.decodeFromString(FakeDataSource.authors)
override suspend fun getAuthors(ids: List<Int>?): List<NetworkAuthor> =
networkJson.decodeFromString<List<NetworkAuthor>>(FakeDataSource.authors)
.matchIds(
ids = ids,
idGetter = NetworkAuthor::id
)
override suspend fun getNewsResources(itemsPerPage: Int): List<NetworkNewsResource> =
networkJson.decodeFromString(FakeDataSource.data)
override suspend fun getNewsResources(ids: List<Int>?): List<NetworkNewsResource> =
networkJson.decodeFromString<List<NetworkNewsResource>>(FakeDataSource.data)
.matchIds(
ids = ids,
idGetter = NetworkNewsResource::id
)
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =
getTopics(ids = null).mapToChangeList(
after = after,
idGetter = NetworkTopic::id
)
override suspend fun getAuthorChangeList(after: Int?): List<NetworkChangeList> =
getAuthors(ids = null).mapToChangeList(
after = after,
idGetter = NetworkAuthor::id
)
override suspend fun getNewsResourceChangeList(after: Int?): List<NetworkChangeList> =
getNewsResources(ids = null).mapToChangeList(
after = after,
idGetter = NetworkNewsResource::id
)
}
/**
* Return items from [this] whose id defined by [idGetter] is in [ids] if [ids] is not null
*/
private fun <T> List<T>.matchIds(
ids: List<Int>?,
idGetter: (T) -> Int
) = when (ids) {
null -> this
else -> ids.toSet().let { idSet -> this.filter { idSet.contains(idGetter(it)) } }
}
/**
* Maps items to a change list where the change list version is denoted by the index of each item.
* [after] simulates which models have changed by excluding items before it
*/
private fun <T> List<T>.mapToChangeList(
after: Int?,
idGetter: (T) -> Int
) = mapIndexed { index, item ->
NetworkChangeList(
id = idGetter(item),
changeListVersion = index,
isDelete = false,
)
}.drop(after ?: 0)

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.core.network
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
@ -24,9 +25,15 @@ import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
* Interface representing network calls to the NIA backend
*/
interface NiANetwork {
suspend fun getTopics(itemsPerPage: Int = 200): List<NetworkTopic>
suspend fun getTopics(ids: List<Int>? = null): List<NetworkTopic>
suspend fun getAuthors(itemsPerPage: Int = 200): List<NetworkAuthor>
suspend fun getAuthors(ids: List<Int>? = null): List<NetworkAuthor>
suspend fun getNewsResources(itemsPerPage: Int = 200): List<NetworkNewsResource>
suspend fun getNewsResources(ids: List<Int>? = null): List<NetworkNewsResource>
suspend fun getTopicChangeList(after: Int? = null): List<NetworkChangeList>
suspend fun getAuthorChangeList(after: Int? = null): List<NetworkChangeList>
suspend fun getNewsResourceChangeList(after: Int? = null): List<NetworkChangeList>
}

@ -20,6 +20,7 @@ import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
@ -35,18 +36,41 @@ class FakeNiANetwork @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val networkJson: Json
) : NiANetwork {
override suspend fun getTopics(itemsPerPage: Int): List<NetworkTopic> =
override suspend fun getTopics(ids: List<Int>?): List<NetworkTopic> =
withContext(ioDispatcher) {
networkJson.decodeFromString(FakeDataSource.topicsData)
}
override suspend fun getNewsResources(itemsPerPage: Int): List<NetworkNewsResource> =
override suspend fun getNewsResources(ids: List<Int>?): List<NetworkNewsResource> =
withContext(ioDispatcher) {
networkJson.decodeFromString(FakeDataSource.data)
}
override suspend fun getAuthors(itemsPerPage: Int): List<NetworkAuthor> =
override suspend fun getAuthors(ids: List<Int>?): List<NetworkAuthor> =
withContext(ioDispatcher) {
networkJson.decodeFromString(FakeDataSource.authors)
}
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =
getTopics().mapToChangeList(NetworkTopic::id)
override suspend fun getAuthorChangeList(after: Int?): List<NetworkChangeList> =
getAuthors().mapToChangeList(NetworkAuthor::id)
override suspend fun getNewsResourceChangeList(after: Int?): List<NetworkChangeList> =
getNewsResources().mapToChangeList(NetworkNewsResource::id)
}
/**
* Converts a list of [T] to change list of all the items in it where [idGetter] defines the
* [NetworkChangeList.id]
*/
private fun <T> List<T>.mapToChangeList(
idGetter: (T) -> Int
) = mapIndexed { index, item ->
NetworkChangeList(
id = idGetter(item),
changeListVersion = index,
isDelete = false,
)
}

@ -0,0 +1,29 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.network.model
import kotlinx.serialization.Serializable
/**
* Network representation of a change list for a model
*/
@Serializable
data class NetworkChangeList(
val id: Int,
val changeListVersion: Int,
val isDelete: Boolean,
)

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.network.retrofit
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
@ -38,18 +39,33 @@ import retrofit2.http.Query
private interface RetrofitNiANetworkApi {
@GET(value = "topics")
suspend fun getTopics(
@Query("pageSize") itemsPerPage: Int,
@Query("id") ids: List<Int>?,
): NetworkResponse<List<NetworkTopic>>
@GET(value = "authors")
suspend fun getAuthors(
@Query("pageSize") itemsPerPage: Int,
@Query("id") ids: List<Int>?,
): NetworkResponse<List<NetworkAuthor>>
@GET(value = "newsresources")
suspend fun getNewsResources(
@Query("pageSize") itemsPerPage: Int,
@Query("id") ids: List<Int>?,
): NetworkResponse<List<NetworkNewsResource>>
@GET(value = "changelists/topics")
suspend fun getTopicChangeList(
@Query("after") after: Int?,
): List<NetworkChangeList>
@GET(value = "changelists/authors")
suspend fun getAuthorsChangeList(
@Query("after") after: Int?,
): List<NetworkChangeList>
@GET(value = "changelists/newsresources")
suspend fun getNewsResourcesChangeList(
@Query("after") after: Int?,
): List<NetworkChangeList>
}
private const val NiABaseUrl = "https://staging-url.com/"
@ -86,12 +102,21 @@ class RetrofitNiANetwork @Inject constructor(
.build()
.create(RetrofitNiANetworkApi::class.java)
override suspend fun getTopics(itemsPerPage: Int): List<NetworkTopic> =
networkApi.getTopics(itemsPerPage = itemsPerPage).data
override suspend fun getTopics(ids: List<Int>?): List<NetworkTopic> =
networkApi.getTopics(ids = ids).data
override suspend fun getAuthors(ids: List<Int>?): List<NetworkAuthor> =
networkApi.getAuthors(ids = ids).data
override suspend fun getNewsResources(ids: List<Int>?): List<NetworkNewsResource> =
networkApi.getNewsResources(ids = ids).data
override suspend fun getTopicChangeList(after: Int?): List<NetworkChangeList> =
networkApi.getTopicChangeList(after = after)
override suspend fun getAuthors(itemsPerPage: Int): List<NetworkAuthor> =
networkApi.getAuthors(itemsPerPage = itemsPerPage).data
override suspend fun getAuthorChangeList(after: Int?): List<NetworkChangeList> =
networkApi.getAuthorsChangeList(after = after)
override suspend fun getNewsResources(itemsPerPage: Int): List<NetworkNewsResource> =
networkApi.getNewsResources(itemsPerPage = itemsPerPage).data
override suspend fun getNewsResourceChangeList(after: Int?): List<NetworkChangeList> =
networkApi.getNewsResourcesChangeList(after = after)
}

Loading…
Cancel
Save