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>
}

@ -27,9 +27,9 @@ import org.intellij.lang.annotations.Language
object FakeDataSource {
val sampleTopic = NetworkTopic(
id = 1,
name = "UI",
name = "UI",
shortDescription = "Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets",
longDescription = "Learn how to optimize your app's user interface - everything that users can see and interact with. Stay up to date on tocpis such as Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets, and many more!",
longDescription = "Learn how to optimize your app's user interface - everything that users can see and interact with. Stay up to date on tocpis such as Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets, and many more!",
url = "url",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_UI.svg?alt=media&token=5d1d25a8-db1b-4cf1-9706-82ba0d133bf9"
)
@ -1638,4 +1638,472 @@ object FakeDataSource {
}
]
""".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