Add AuthorsRepository to data the layer with sync

Change-Id: I5b9ba0508058332dfa153d24662a95553aa7299e
pull/2/head
Adetunji Dahunsi 2 years ago committed by Don Turner
parent 04157c37da
commit 6f1206ef92

@ -0,0 +1,401 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "fdb65d28086c5a10d129b905ce940aa4",
"entities": [
{
"tableName": "authors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `image_url` TEXT NOT NULL, `twitter` TEXT NOT NULL DEFAULT '', `medium_page` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "imageUrl",
"columnName": "image_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "twitter",
"columnName": "twitter",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "mediumPage",
"columnName": "medium_page",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_authors_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_authors_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "episodes_authors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`episode_id` INTEGER NOT NULL, `author_id` INTEGER NOT NULL, PRIMARY KEY(`episode_id`, `author_id`), FOREIGN KEY(`episode_id`) REFERENCES `episodes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`author_id`) REFERENCES `authors`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "episodeId",
"columnName": "episode_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorId",
"columnName": "author_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"episode_id",
"author_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "episodes",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"episode_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "authors",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"author_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "episodes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `publish_date` INTEGER NOT NULL, `alternate_video` TEXT, `alternate_audio` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "publishDate",
"columnName": "publish_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alternateVideo",
"columnName": "alternate_video",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "alternateAudio",
"columnName": "alternate_audio",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "news_resources_authors",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`news_resource_id` INTEGER NOT NULL, `author_id` INTEGER NOT NULL, PRIMARY KEY(`news_resource_id`, `author_id`), FOREIGN KEY(`news_resource_id`) REFERENCES `news_resources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`author_id`) REFERENCES `authors`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "newsResourceId",
"columnName": "news_resource_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorId",
"columnName": "author_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"news_resource_id",
"author_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "news_resources",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"news_resource_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "authors",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"author_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "news_resources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `episode_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `url` TEXT NOT NULL, `header_image_url` TEXT, `publish_date` INTEGER NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`episode_id`) REFERENCES `episodes`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "episodeId",
"columnName": "episode_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "headerImageUrl",
"columnName": "header_image_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "publishDate",
"columnName": "publish_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "episodes",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"episode_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "news_resources_topics",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`news_resource_id` INTEGER NOT NULL, `topic_id` INTEGER NOT NULL, PRIMARY KEY(`news_resource_id`, `topic_id`), FOREIGN KEY(`news_resource_id`) REFERENCES `news_resources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`topic_id`) REFERENCES `topics`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "newsResourceId",
"columnName": "news_resource_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "topicId",
"columnName": "topic_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"news_resource_id",
"topic_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "news_resources",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"news_resource_id"
],
"referencedColumns": [
"id"
]
},
{
"table": "topics",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"topic_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "topics",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL DEFAULT '', `url` TEXT NOT NULL DEFAULT '', `imageUrl` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "shortDescription",
"columnName": "shortDescription",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "longDescription",
"columnName": "longDescription",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "imageUrl",
"columnName": "imageUrl",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_topics_name",
"unique": true,
"columnNames": [
"name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_topics_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fdb65d28086c5a10d129b905ce940aa4')"
]
}
}

@ -44,11 +44,12 @@ import com.google.samples.apps.nowinandroid.core.database.util.NewsResourceTypeC
NewsResourceTopicCrossRef::class,
TopicEntity::class,
],
version = 4,
version = 5,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3, spec = DatabaseMigrations.Schema2to3::class),
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5),
],
exportSchema = true,
)

@ -38,10 +38,16 @@ data class AuthorEntity(
val name: String,
@ColumnInfo(name = "image_url")
val imageUrl: String,
@ColumnInfo(defaultValue = "")
val twitter: String,
@ColumnInfo(name = "medium_page", defaultValue = "")
val mediumPage: String,
)
fun AuthorEntity.asExternalModel() = Author(
id = id,
name = name,
imageUrl = imageUrl,
twitter = twitter,
mediumPage = mediumPage,
)

@ -17,8 +17,10 @@
package com.google.samples.apps.nowinandroid.core.domain.test
import com.google.samples.apps.nowinandroid.core.domain.di.DomainModule
import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.fake.FakeAuthorsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.fake.FakeNewsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.fake.FakeTopicsRepository
import dagger.Binds
@ -37,6 +39,11 @@ interface TestDomainModule {
fakeTopicsRepository: FakeTopicsRepository
): TopicsRepository
@Binds
fun bindsAuthorRepository(
fakeAuthorsRepository: FakeAuthorsRepository
): AuthorsRepository
@Binds
fun bindsNewsResourceRepository(
fakeNewsRepository: FakeNewsRepository

@ -16,6 +16,8 @@
package com.google.samples.apps.nowinandroid.core.domain.di
import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.LocalAuthorsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.LocalNewsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.LocalTopicsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
@ -34,6 +36,11 @@ interface DomainModule {
topicsRepository: LocalTopicsRepository
): TopicsRepository
@Binds
fun bindsAuthorsRepository(
authorsRepository: LocalAuthorsRepository
): AuthorsRepository
@Binds
fun bindsNewsResourceRepository(
newsRepository: LocalNewsRepository

@ -22,5 +22,7 @@ import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
fun NetworkAuthor.asEntity() = AuthorEntity(
id = id,
name = name,
imageUrl = imageUrl
imageUrl = imageUrl,
twitter = twitter,
mediumPage = mediumPage,
)

@ -69,6 +69,8 @@ fun NetworkNewsResource.authorEntityShells() =
id = authorId,
name = "",
imageUrl = "",
twitter = "",
mediumPage = "",
)
}

@ -0,0 +1,33 @@
/*
* 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.domain.repository
import com.google.samples.apps.nowinandroid.core.model.data.Author
import kotlinx.coroutines.flow.Flow
interface AuthorsRepository {
/**
* Gets the available Authors as a stream
*/
fun getAuthorsStream(): Flow<List<Author>>
/**
* Synchronizes the local database in backing the repository with the network.
* Returns if the sync was successful or not.
*/
suspend fun sync(): Boolean
}

@ -0,0 +1,50 @@
/*
* 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.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.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
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/**
* Room database backed implementation of the [AuthorsRepository].
*/
class LocalAuthorsRepository @Inject constructor(
private val authorDao: AuthorDao,
private val network: NiANetwork,
) : 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
}

@ -0,0 +1,60 @@
/*
* 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.domain.repository.fake
import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
import com.google.samples.apps.nowinandroid.core.network.fake.FakeDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
/**
* Fake implementation of the [AuthorsRepository] that retrieves the Authors from a JSON String, and
* uses a local DataStore instance to save and retrieve followed Author ids.
*
* This allows us to run the app with fake data, without needing an internet connection or working
* backend.
*/
class FakeAuthorsRepository @Inject constructor(
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
private val networkJson: Json,
) : AuthorsRepository {
override fun getAuthorsStream(): Flow<List<Author>> = flow {
emit(
networkJson.decodeFromString<List<NetworkAuthor>>(FakeDataSource.authors).map {
Author(
id = it.id,
name = it.name,
imageUrl = it.imageUrl,
twitter = it.twitter,
mediumPage = it.mediumPage,
)
}
)
}
.flowOn(ioDispatcher)
override suspend fun sync() = true
}

@ -51,7 +51,9 @@ class PopulatedEpisodeKtTest {
AuthorEntity(
id = 2,
name = "name",
imageUrl = "imageUrl"
imageUrl = "imageUrl",
twitter = "twitter",
mediumPage = "mediumPage",
)
),
)
@ -82,7 +84,9 @@ class PopulatedEpisodeKtTest {
Author(
id = 2,
name = "name",
imageUrl = "imageUrl"
imageUrl = "imageUrl",
twitter = "twitter",
mediumPage = "mediumPage",
)
),
),

@ -49,7 +49,9 @@ class PopulatedNewsResourceKtTest {
AuthorEntity(
id = 2,
name = "name",
imageUrl = "imageUrl"
imageUrl = "imageUrl",
twitter = "twitter",
mediumPage = "mediumPage",
)
),
topics = listOf(
@ -79,7 +81,9 @@ class PopulatedNewsResourceKtTest {
Author(
id = 2,
name = "name",
imageUrl = "imageUrl"
imageUrl = "imageUrl",
twitter = "twitter",
mediumPage = "mediumPage",
)
),
topics = listOf(

@ -34,7 +34,9 @@ class NetworkEntityKtTest {
val networkModel = NetworkAuthor(
id = 0,
name = "Test",
imageUrl = "something"
imageUrl = "something",
twitter = "twitter",
mediumPage = "mediumPage",
)
val entity = networkModel.asEntity()

@ -0,0 +1,80 @@
/*
* 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.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.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.Test
class LocalAuthorsRepositoryTest {
private lateinit var subject: LocalAuthorsRepository
private lateinit var authorDao: AuthorDao
private lateinit var network: NiANetwork
@Before
fun setup() {
authorDao = TestAuthorDao()
network = TestNiaNetwork()
subject = LocalAuthorsRepository(
authorDao = authorDao,
network = network,
)
}
@Test
fun localAuthorsRepository_Authors_stream_is_backed_by_Authors_dao() =
runTest {
Assert.assertEquals(
authorDao.getAuthorEntitiesStream()
.first()
.map(AuthorEntity::asExternalModel),
subject.getAuthorsStream()
.first()
)
}
@Test
fun localAuthorsRepository_sync_pulls_from_network() =
runTest {
subject.sync()
val network = network.getAuthors()
.map(NetworkAuthor::asEntity)
val db = authorDao.getAuthorEntitiesStream()
.first()
Assert.assertEquals(
network.map(AuthorEntity::id),
db.map(AuthorEntity::id)
)
}
}

@ -32,6 +32,8 @@ class TestAuthorDao : AuthorDao {
id = 1,
name = "Topic",
imageUrl = "imageUrl",
twitter = "twitter",
mediumPage = "mediumPage",
)
)
)

@ -110,7 +110,9 @@ private fun NewsResourceEntity.asPopulatedNewsResource() = PopulatedNewsResource
AuthorEntity(
id = 2,
name = "name",
imageUrl = "imageUrl"
imageUrl = "imageUrl",
twitter = "twitter",
mediumPage = "mediumPage",
)
),
topics = listOf(

@ -18,6 +18,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.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.serialization.decodeFromString
@ -33,6 +34,9 @@ class TestNiaNetwork : NiANetwork {
override suspend fun getTopics(itemsPerPage: Int): List<NetworkTopic> =
networkJson.decodeFromString(FakeDataSource.topicsData)
override suspend fun getAuthors(itemsPerPage: Int): List<NetworkAuthor> =
networkJson.decodeFromString(FakeDataSource.authors)
override suspend fun getNewsResources(itemsPerPage: Int): List<NetworkNewsResource> =
networkJson.decodeFromString(FakeDataSource.data)
}

@ -23,4 +23,6 @@ data class Author(
val id: Int,
val name: String,
val imageUrl: String,
val twitter: String,
val mediumPage: String,
)

@ -16,6 +16,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.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
@ -25,5 +26,7 @@ import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
interface NiANetwork {
suspend fun getTopics(itemsPerPage: Int = 200): List<NetworkTopic>
suspend fun getAuthors(itemsPerPage: Int = 200): List<NetworkAuthor>
suspend fun getNewsResources(itemsPerPage: Int = 200): List<NetworkNewsResource>
}

@ -1636,6 +1636,474 @@ object FakeDataSource {
35
]
}
]
""".trimIndent()
@Language("JSON")
val authors = """
[
{
"id": "1",
"name": "Márton Braun",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "2",
"name": "Greg Hartrell",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "3",
"name": "Simona Stojanovic",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "4",
"name": "Andrew Flynn",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "5",
"name": "Jon Boekenoogen",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "6",
"name": "Florina Muntenescu",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "7",
"name": "Lidia Gaymond",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "8",
"name": "Vicki Amin",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "9",
"name": "Marcel Pintó",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "10",
"name": "Krish Vitaldevara",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "11",
"name": "Gerry Fan",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "12",
"name": "Pietro Maggi",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "13",
"name": "Rohan Shah",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "14",
"name": "Dave Burke",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "15",
"name": "Meghan Mehta",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "16",
"name": "Anna Bernbaum",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "17",
"name": "Adarsh Fernando",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "18",
"name": "Madan Ankapura",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "19",
"name": "Kateryna Semenova",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "20",
"name": "Rahul Ravikumar",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "21",
"name": "Chris Craik",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "22",
"name": "Marcel Pintó Biescas",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "23",
"name": "Alex Vanyo",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "24",
"name": "Manuel Vicente Vivo",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "25",
"name": "Arjun Dayal",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "26",
"name": "Murat Yener",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "27",
"name": "Alex Saveau",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "28",
"name": "Paul Lammertsma",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "29",
"name": "Caren Chang",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "30",
"name": "Mayuri Khinvasara Khabya",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "31",
"name": "Romain Guy",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "32",
"name": "Chet Hasse",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "33",
"name": "Tor Norbye",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "34",
"name": "Nicole Laure",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "35",
"name": "Yigit Boyar",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "36",
"name": "Sean McQuillan",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "37",
"name": "Ben Weiss",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "38",
"name": "Chet Haase",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "39",
"name": "Carmen Jackson",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "40",
"name": "Manuel Vivo",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "41",
"name": "TJ Dahunsi",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "42",
"name": "Shailen Tuli",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "43",
"name": "Murat",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "44",
"name": "Kailiang Chen",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "45",
"name": "Meghan",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "46",
"name": "Jeremy Walker",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "47",
"name": "Don Turner",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "48",
"name": "Lilian Young",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "49",
"name": "Wenhung Teng",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "50",
"name": "Charcoal Chen",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "51",
"name": "Mike Yerou",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "52",
"name": "Peter Visontay",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "53",
"name": "Marcelo Hernandez",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "54",
"name": "Daniel Santiago",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "55",
"name": "Brad Corso",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "56",
"name": "Jonathan Koren",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "57",
"name": "Anna-Chiara Bellini",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "58",
"name": "Amanda Alexander",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "59",
"name": "Android Developers Backstage",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "60",
"name": "Nicole Borrelli",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "61",
"name": "Dan Saadati",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "62",
"name": "Nick Butcher",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "63",
"name": "Ian Lake",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "64",
"name": "Diana Wong",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "65",
"name": "Patricia Correa",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
},
{
"id": "66",
"name": "The Modern Android Development Team",
"mediumPage": "",
"twitter": "",
"imageUrl": ""
}
]
""".trimIndent()
}

@ -19,6 +19,7 @@ package com.google.samples.apps.nowinandroid.core.network.fake
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.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
@ -43,4 +44,9 @@ class FakeNiANetwork @Inject constructor(
withContext(ioDispatcher) {
networkJson.decodeFromString(FakeDataSource.data)
}
override suspend fun getAuthors(itemsPerPage: Int): List<NetworkAuthor> =
withContext(ioDispatcher) {
networkJson.decodeFromString(FakeDataSource.authors)
}
}

@ -27,4 +27,6 @@ data class NetworkAuthor(
val id: Int,
val name: String,
val imageUrl: String,
val twitter: String,
val mediumPage: String,
)

@ -17,6 +17,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.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
@ -40,6 +41,11 @@ private interface RetrofitNiANetworkApi {
@Query("pageSize") itemsPerPage: Int,
): NetworkResponse<List<NetworkTopic>>
@GET(value = "authors")
suspend fun getAuthors(
@Query("pageSize") itemsPerPage: Int,
): NetworkResponse<List<NetworkAuthor>>
@GET(value = "newsresources")
suspend fun getNewsResources(
@Query("pageSize") itemsPerPage: Int,
@ -83,6 +89,9 @@ class RetrofitNiANetwork @Inject constructor(
override suspend fun getTopics(itemsPerPage: Int): List<NetworkTopic> =
networkApi.getTopics(itemsPerPage = itemsPerPage).data
override suspend fun getAuthors(itemsPerPage: Int): List<NetworkAuthor> =
networkApi.getAuthors(itemsPerPage = itemsPerPage).data
override suspend fun getNewsResources(itemsPerPage: Int): List<NetworkNewsResource> =
networkApi.getNewsResources(itemsPerPage = itemsPerPage).data
}

@ -284,7 +284,9 @@ private val newsResource = NewsResource(
Author(
id = 1,
name = "Name",
imageUrl = ""
imageUrl = "",
twitter = "",
mediumPage = "",
)
),
topics = listOf(

@ -24,6 +24,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkerParameters
import com.google.samples.apps.nowinandroid.core.domain.repository.AuthorsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.NewsRepository
import com.google.samples.apps.nowinandroid.core.domain.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.sync.SyncRepository
@ -32,6 +33,9 @@ import com.google.samples.apps.nowinandroid.sync.initializers.syncForegroundInfo
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
/**
* Syncs the data layer by delegating to the appropriate repository instances with
@ -44,14 +48,21 @@ class SyncWorker @AssistedInject constructor(
private val syncRepository: SyncRepository,
private val topicRepository: TopicsRepository,
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository,
) : CoroutineWorker(appContext, workerParams) {
override suspend fun getForegroundInfo(): ForegroundInfo =
appContext.syncForegroundInfo()
override suspend fun doWork(): Result =
// First sync the repositories
when (topicRepository.sync() && newsRepository.sync()) {
override suspend fun doWork(): Result = coroutineScope {
// First sync the repositories in parallel
val syncedSuccessfully = awaitAll(
async { topicRepository.sync() },
async { authorsRepository.sync() },
async { newsRepository.sync() },
).all { it }
when (syncedSuccessfully) {
// Sync ran successfully, notify the SyncRepository that sync has been run
true -> {
syncRepository.notifyFirstTimeSyncRun()
@ -59,6 +70,7 @@ class SyncWorker @AssistedInject constructor(
}
false -> Result.retry()
}
}
companion object {
private const val SyncInterval = 1L

Loading…
Cancel
Save