Integrating data layer DAOs

Change-Id: I6bebf14768841f0fc9a224d933c059cfc50f9943
pull/2/head
Adetunji Dahunsi 2 years ago
parent 1411b1576b
commit 9651c6a581

@ -23,15 +23,15 @@ import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
import com.google.samples.apps.nowinandroid.core.database.dao.EpisodeDao
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import com.google.samples.apps.nowinandroid.core.database.model.EpisodeAuthorCrossRef
import com.google.samples.apps.nowinandroid.core.database.model.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceAuthorCrossRef
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.util.InstantConverter
import com.google.samples.apps.nowinandroid.core.database.util.NewsResourceTypeConverter
import com.google.samples.apps.nowinandroid.core.model.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeAuthorCrossRef
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceAuthorCrossRef
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.core.model.entities.TopicEntity
@Database(
entities = [

@ -19,7 +19,7 @@ package com.google.samples.apps.nowinandroid.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.google.samples.apps.nowinandroid.core.model.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import kotlinx.coroutines.flow.Flow
/**

@ -19,8 +19,9 @@ package com.google.samples.apps.nowinandroid.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.google.samples.apps.nowinandroid.core.database.model.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedEpisode
import com.google.samples.apps.nowinandroid.core.model.data.Episode
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeEntity
import kotlinx.coroutines.flow.Flow
/**
@ -29,7 +30,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface EpisodeDao {
@Query(value = "SELECT * FROM episodes")
fun getEpisodesStream(): Flow<List<Episode>>
fun getEpisodesStream(): Flow<List<PopulatedEpisode>>
@Insert
suspend fun saveEpisodeEntities(entities: List<EpisodeEntity>)

@ -19,8 +19,9 @@ package com.google.samples.apps.nowinandroid.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.database.model.PopulatedNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceEntity
import kotlinx.coroutines.flow.Flow
/**
@ -29,7 +30,7 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface NewsResourceDao {
@Query(value = "SELECT * FROM news_resources")
fun getNewsResourcesStream(): Flow<List<NewsResource>>
fun getNewsResourcesStream(): Flow<List<PopulatedNewsResource>>
@Query(
value = """
@ -37,7 +38,7 @@ interface NewsResourceDao {
WHERE id IN (:filterTopicIds)
"""
)
fun getNewsResourcesStream(filterTopicIds: Set<Int>): Flow<List<NewsResource>>
fun getNewsResourcesStream(filterTopicIds: Set<Int>): Flow<List<PopulatedNewsResource>>
@Insert
suspend fun saveNewsResourceEntities(entities: List<NewsResourceEntity>)

@ -19,7 +19,7 @@ package com.google.samples.apps.nowinandroid.core.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.google.samples.apps.nowinandroid.core.model.entities.TopicEntity
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import kotlinx.coroutines.flow.Flow
/**

@ -14,12 +14,13 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.entities
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.samples.apps.nowinandroid.core.model.data.Author
/**
* Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
@ -38,3 +39,9 @@ data class AuthorEntity(
@ColumnInfo(name = "image_url")
val imageUrl: String,
)
fun AuthorEntity.asExternalModel() = Author(
id = id,
name = name,
imageUrl = imageUrl,
)

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.entities
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.entities
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.entities
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity

@ -14,12 +14,14 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.entities
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import kotlinx.datetime.Instant
/**
@ -47,5 +49,17 @@ data class NewsResourceEntity(
val url: String,
@ColumnInfo(name = "publish_date")
val publishDate: Instant,
val type: String,
val type: NewsResourceType,
)
fun NewsResourceEntity.asExternalModel() = NewsResource(
id = id,
episodeId = episodeId,
title = title,
content = content,
url = url,
publishDate = publishDate,
type = type,
authors = listOf(),
topics = listOf()
)

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.entities
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.ColumnInfo
import androidx.room.Entity

@ -0,0 +1,55 @@
/*
* 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.database.model
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import com.google.samples.apps.nowinandroid.core.model.data.Episode
/**
* External data layer representation of an NiA episode
*/
data class PopulatedEpisode(
@Embedded
val entity: EpisodeEntity,
@Relation(
parentColumn = "id",
entityColumn = "episode_id"
)
val newsResources: List<NewsResourceEntity>,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = EpisodeAuthorCrossRef::class,
parentColumn = "episode_id",
entityColumn = "author_id",
)
)
val authors: List<AuthorEntity>
)
fun PopulatedEpisode.asExternalModel() = Episode(
id = entity.id,
name = entity.name,
publishDate = entity.publishDate,
alternateVideo = entity.alternateVideo,
alternateAudio = entity.alternateAudio,
newsResources = newsResources.map(NewsResourceEntity::asExternalModel),
authors = authors.map(AuthorEntity::asExternalModel)
)

@ -0,0 +1,67 @@
/*
* 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.database.model
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
/**
* External data layer representation of a fully populated NiA news resource
*/
data class PopulatedNewsResource(
@Embedded
val entity: NewsResourceEntity,
@Relation(
parentColumn = "episode_id",
entityColumn = "id"
)
val episode: EpisodeEntity,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = NewsResourceAuthorCrossRef::class,
parentColumn = "news_resource_id",
entityColumn = "author_id",
)
)
val authors: List<AuthorEntity>,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = NewsResourceTopicCrossRef::class,
parentColumn = "news_resource_id",
entityColumn = "topic_id",
)
)
val topics: List<TopicEntity>
)
fun PopulatedNewsResource.asExternalModel() = NewsResource(
id = entity.id,
episodeId = entity.episodeId,
title = entity.title,
content = entity.content,
url = entity.url,
publishDate = entity.publishDate,
type = entity.type,
authors = authors.map(AuthorEntity::asExternalModel),
topics = topics.map(TopicEntity::asExternalModel)
)

@ -14,11 +14,12 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.entities
package com.google.samples.apps.nowinandroid.core.database.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.samples.apps.nowinandroid.core.model.data.Topic
/**
* Defines a topic a user may follow.
@ -37,3 +38,10 @@ data class TopicEntity(
val description: String,
val followed: Boolean,
)
fun TopicEntity.asExternalModel() = Topic(
id = id,
name = name,
description = description,
followed = followed,
)

@ -18,6 +18,7 @@ package com.google.samples.apps.nowinandroid.core.database.util
import androidx.room.TypeConverter
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.asNewsResourceType
import kotlinx.datetime.Instant
class InstantConverter {
@ -36,10 +37,5 @@ class NewsResourceTypeConverter {
value?.let(NewsResourceType::name)
@TypeConverter
fun stringToNewsResourceType(name: String?): NewsResourceType = when (name) {
null -> NewsResourceType.Unknown
else -> NewsResourceType.values()
.firstOrNull { type -> type.name == name }
?: NewsResourceType.Unknown
}
fun stringToNewsResourceType(name: String?): NewsResourceType = name.asNewsResourceType()
}

@ -39,11 +39,13 @@ android {
dependencies {
implementation project(':core-model')
implementation project(':core-database')
implementation project(':core-datastore')
implementation project(':core-network')
testImplementation project(':core-testing')
implementation libs.kotlinx.datetime
implementation libs.kotlinx.coroutines.android
implementation libs.kotlinx.serialization.json

@ -0,0 +1,26 @@
/*
* 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.model
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
fun NetworkAuthor.asEntity() = AuthorEntity(
id = id,
name = name,
imageUrl = imageUrl
)

@ -0,0 +1,37 @@
/*
* 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.model
import com.google.samples.apps.nowinandroid.core.database.model.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.network.model.NetworkEpisode
import com.google.samples.apps.nowinandroid.core.network.model.NetworkEpisodeExpanded
fun NetworkEpisode.asEntity() = EpisodeEntity(
id = id,
name = name,
publishDate = publishDate,
alternateVideo = alternateVideo,
alternateAudio = alternateAudio,
)
fun NetworkEpisodeExpanded.asEntity() = EpisodeEntity(
id = id,
name = name,
publishDate = publishDate,
alternateVideo = alternateVideo,
alternateAudio = alternateAudio,
)

@ -0,0 +1,41 @@
/*
* 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.model
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded
fun NetworkNewsResource.asEntity() = NewsResourceEntity(
id = id,
episodeId = episodeId,
title = title,
content = content,
url = url,
publishDate = publishDate,
type = type,
)
fun NetworkNewsResourceExpanded.asEntity() = NewsResourceEntity(
id = id,
episodeId = episodeId,
title = title,
content = content,
url = url,
publishDate = publishDate,
type = type,
)

@ -0,0 +1,27 @@
/*
* 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.model
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
fun NetworkTopic.asEntity() = TopicEntity(
id = id,
name = name,
description = description,
followed = followed
)

@ -18,9 +18,9 @@ package com.google.samples.apps.nowinandroid.core.domain.repository
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferences
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.model.network.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers
import com.google.samples.apps.nowinandroid.core.network.fake.FakeDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

@ -0,0 +1,90 @@
/*
* 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.database.model
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.Episode
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import kotlinx.datetime.Instant
import org.junit.Assert.assertEquals
import org.junit.Test
class PopulatedEpisodeKtTest {
@Test
fun populated_episode_can_be_mapped_to_episode() {
val populatedEpisode = PopulatedEpisode(
entity = EpisodeEntity(
id = 0,
name = "Test",
publishDate = Instant.fromEpochMilliseconds(1),
alternateAudio = "audio",
alternateVideo = "video"
),
newsResources = listOf(
NewsResourceEntity(
id = 1,
episodeId = 0,
title = "news",
content = "Hilt",
url = "url",
type = Video,
publishDate = Instant.fromEpochMilliseconds(1),
)
),
authors = listOf(
AuthorEntity(
id = 2,
name = "name",
imageUrl = "imageUrl"
)
),
)
val episode = populatedEpisode.asExternalModel()
assertEquals(
Episode(
id = 0,
name = "Test",
publishDate = Instant.fromEpochMilliseconds(1),
alternateAudio = "audio",
alternateVideo = "video",
newsResources = listOf(
NewsResource(
id = 1,
episodeId = 0,
title = "news",
content = "Hilt",
url = "url",
type = Video,
publishDate = Instant.fromEpochMilliseconds(1),
authors = listOf(),
topics = listOf()
)
),
authors = listOf(
Author(
id = 2,
name = "name",
imageUrl = "imageUrl"
)
),
),
episode
)
}
}

@ -0,0 +1,93 @@
/*
* 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.database.model
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlinx.datetime.Instant
import org.junit.Assert.assertEquals
import org.junit.Test
class PopulatedNewsResourceKtTest {
@Test
fun populated_news_resource_can_be_mapped_to_news_resource() {
val populatedNewsResource = PopulatedNewsResource(
entity = NewsResourceEntity(
id = 1,
episodeId = 0,
title = "news",
content = "Hilt",
url = "url",
type = Video,
publishDate = Instant.fromEpochMilliseconds(1),
),
episode = EpisodeEntity(
id = 4,
name = "episode 4",
publishDate = Instant.fromEpochMilliseconds(2),
alternateAudio = "audio",
alternateVideo = "video",
),
authors = listOf(
AuthorEntity(
id = 2,
name = "name",
imageUrl = "imageUrl"
)
),
topics = listOf(
TopicEntity(
id = 3,
name = "name",
description = "description",
followed = true,
)
),
)
val newsResource = populatedNewsResource.asExternalModel()
assertEquals(
NewsResource(
id = 1,
episodeId = 0,
title = "news",
content = "Hilt",
url = "url",
type = Video,
publishDate = Instant.fromEpochMilliseconds(1),
authors = listOf(
Author(
id = 2,
name = "name",
imageUrl = "imageUrl"
)
),
topics = listOf(
Topic(
id = 3,
name = "name",
description = "description",
followed = true,
)
)
),
newsResource
)
}
}

@ -14,16 +14,15 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.network.NetworkAuthor
import com.google.samples.apps.nowinandroid.core.model.network.NetworkEpisode
import com.google.samples.apps.nowinandroid.core.model.network.NetworkEpisodeExpanded
import com.google.samples.apps.nowinandroid.core.model.network.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.model.network.NetworkNewsResourceExpanded
import com.google.samples.apps.nowinandroid.core.model.network.NetworkTopic
import com.google.samples.apps.nowinandroid.core.model.network.asEntity
package com.google.samples.apps.nowinandroid.core.domain.model
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
import com.google.samples.apps.nowinandroid.core.network.model.NetworkEpisode
import com.google.samples.apps.nowinandroid.core.network.model.NetworkEpisodeExpanded
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResourceExpanded
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import kotlinx.datetime.Instant
import org.junit.Assert.assertEquals
import org.junit.Test
@ -60,15 +59,16 @@ class NetworkEntityKtTest {
@Test
fun network_news_resource_can_be_mapped_to_news_resource_entity() {
val networkModel = NetworkNewsResource(
id = 0,
episodeId = 2,
title = "title",
content = "content",
url = "url",
publishDate = Instant.fromEpochMilliseconds(1),
type = NewsResourceType.Article.displayText,
)
val networkModel =
NetworkNewsResource(
id = 0,
episodeId = 2,
title = "title",
content = "content",
url = "url",
publishDate = Instant.fromEpochMilliseconds(1),
type = Article,
)
val entity = networkModel.asEntity()
assertEquals(0, entity.id)
@ -77,17 +77,18 @@ class NetworkEntityKtTest {
assertEquals("content", entity.content)
assertEquals("url", entity.url)
assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
assertEquals(NewsResourceType.Article.displayText, entity.type)
val expandedNetworkModel = NetworkNewsResourceExpanded(
id = 0,
episodeId = 2,
title = "title",
content = "content",
url = "url",
publishDate = Instant.fromEpochMilliseconds(1),
type = NewsResourceType.Article.displayText,
)
assertEquals(Article, entity.type)
val expandedNetworkModel =
NetworkNewsResourceExpanded(
id = 0,
episodeId = 2,
title = "title",
content = "content",
url = "url",
publishDate = Instant.fromEpochMilliseconds(1),
type = Article,
)
val entityFromExpanded = expandedNetworkModel.asEntity()
@ -97,7 +98,7 @@ class NetworkEntityKtTest {
assertEquals("content", entityFromExpanded.content)
assertEquals("url", entityFromExpanded.url)
assertEquals(Instant.fromEpochMilliseconds(1), entityFromExpanded.publishDate)
assertEquals(NewsResourceType.Article.displayText, entityFromExpanded.type)
assertEquals(Article, entityFromExpanded.type)
}
@Test
@ -117,13 +118,14 @@ class NetworkEntityKtTest {
assertEquals("alternateAudio", entity.alternateAudio)
assertEquals(Instant.fromEpochMilliseconds(1), entity.publishDate)
val expandedNetworkModel = NetworkEpisodeExpanded(
id = 0,
name = "name",
publishDate = Instant.fromEpochMilliseconds(1),
alternateVideo = "alternateVideo",
alternateAudio = "alternateAudio",
)
val expandedNetworkModel =
NetworkEpisodeExpanded(
id = 0,
name = "name",
publishDate = Instant.fromEpochMilliseconds(1),
alternateVideo = "alternateVideo",
alternateAudio = "alternateAudio",
)
val entityFromExpanded = expandedNetworkModel.asEntity()

@ -0,0 +1,26 @@
/*
* 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.model.data
/**
* External data layer representation of an NiA Author
*/
data class Author(
val id: Int,
val name: String,
val imageUrl: String,
)

@ -16,33 +16,17 @@
package com.google.samples.apps.nowinandroid.core.model.data
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import com.google.samples.apps.nowinandroid.core.model.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeAuthorCrossRef
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceEntity
import kotlinx.datetime.Instant
/**
* External data layer representation of an NiA episode
*/
data class Episode(
@Embedded
val entity: EpisodeEntity,
@Relation(
parentColumn = "id",
entityColumn = "episode_id"
)
val newsResources: List<NewsResourceEntity>,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = EpisodeAuthorCrossRef::class,
parentColumn = "episode_id",
entityColumn = "author_id",
)
)
val authors: List<AuthorEntity>
val id: Int,
val name: String,
val publishDate: Instant,
val alternateVideo: String?,
val alternateAudio: String?,
val newsResources: List<NewsResource>,
val authors: List<Author>
)

@ -16,45 +16,19 @@
package com.google.samples.apps.nowinandroid.core.model.data
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import com.google.samples.apps.nowinandroid.core.model.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceAuthorCrossRef
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.core.model.entities.TopicEntity
import kotlinx.datetime.Instant
/**
* External data layer representation of a fully populated NiA news resource
*/
data class NewsResource(
@Embedded
val entity: NewsResourceEntity,
@Relation(
parentColumn = "episode_id",
entityColumn = "id"
)
val episode: EpisodeEntity,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = NewsResourceAuthorCrossRef::class,
parentColumn = "news_resource_id",
entityColumn = "author_id",
)
)
val authors: List<AuthorEntity>,
@Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(
value = NewsResourceTopicCrossRef::class,
parentColumn = "news_resource_id",
entityColumn = "topic_id",
)
)
val topics: List<TopicEntity>
val id: Int,
val episodeId: Int,
val title: String,
val content: String,
val url: String,
val publishDate: Instant,
val type: NewsResourceType,
val authors: List<Author>,
val topics: List<Topic>
)

@ -20,44 +20,61 @@ package com.google.samples.apps.nowinandroid.core.model.data
* Type for [NewsResource]
*/
enum class NewsResourceType(
val serializedName: String,
val displayText: String,
// TODO: descriptions should probably be string resources
val description: String
) {
Video(
serializedName = "Video 📺",
displayText = "Video 📺",
description = "A video published on YouTube"
),
APIChange(
serializedName = "API change",
displayText = "API change",
description = "An addition, deprecation or change to the Android platform APIs."
),
Article(
serializedName = "Article 📚",
displayText = "Article 📚",
description = "An article, typically on Medium or the official Android blog"
),
Codelab(
serializedName = "Codelab",
displayText = "Codelab",
description = "A new or updated codelab"
),
Podcast(
serializedName = "Podcast 🎙",
displayText = "Podcast 🎙",
description = "A podcast"
),
Docs(
serializedName = "Docs 📑",
displayText = "Docs 📑",
description = "A new or updated piece of documentation"
),
Event(
serializedName = "Event 📆",
displayText = "Event 📆",
description = "Information about a developer event e.g. Android Developer Summit"
),
DAC(
serializedName = "DAC",
displayText = "DAC",
description = "Android version features - Information about features in an Android"
),
Unknown(
serializedName = "Unknown",
displayText = "Unknown",
description = "Unknown"
)
}
fun String?.asNewsResourceType() = when (this) {
null -> NewsResourceType.Unknown
else -> NewsResourceType.values()
.firstOrNull { type -> type.serializedName == this }
?: NewsResourceType.Unknown
}

@ -16,8 +16,8 @@
package com.google.samples.apps.nowinandroid.core.network
import com.google.samples.apps.nowinandroid.core.model.network.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.model.network.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
/**
* Interface representing network calls to the NIA backend

@ -16,8 +16,9 @@
package com.google.samples.apps.nowinandroid.core.network.fake
import com.google.samples.apps.nowinandroid.core.model.network.NetworkTopic
import com.google.samples.apps.nowinandroid.core.model.network.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Video
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
@ -45,7 +46,7 @@ object FakeDataSource {
second = 0,
nanosecond = 0
).toInstant(TimeZone.UTC),
type = "Video \uD83D\uDCFA",
type = Video,
topics = listOf(0),
)

@ -16,10 +16,10 @@
package com.google.samples.apps.nowinandroid.core.network.fake
import com.google.samples.apps.nowinandroid.core.model.network.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.model.network.NetworkTopic
import com.google.samples.apps.nowinandroid.core.network.NiANetwork
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkTopic
import javax.inject.Inject
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable

@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.network
package com.google.samples.apps.nowinandroid.core.network.model
import com.google.samples.apps.nowinandroid.core.model.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.core.model.data.Author
import kotlinx.serialization.Serializable
/**
* Network representation of [AuthorEntity]
* Network representation of [Author]
*/
@Serializable
data class NetworkAuthor(
@ -28,9 +28,3 @@ data class NetworkAuthor(
val name: String,
val imageUrl: String,
)
fun NetworkAuthor.asEntity() = AuthorEntity(
id = id,
name = name,
imageUrl = imageUrl
)

@ -14,19 +14,18 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.network
package com.google.samples.apps.nowinandroid.core.network.model
import androidx.room.PrimaryKey
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.model.data.Episode
import com.google.samples.apps.nowinandroid.core.network.model.util.InstantSerializer
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Network representation of [EpisodeEntity] when fetched from /networkepisodes
* Network representation of [Episode] when fetched from /episodes
*/
@Serializable
data class NetworkEpisode(
@PrimaryKey
val id: Int,
val name: String,
@Serializable(InstantSerializer::class)
@ -38,11 +37,10 @@ data class NetworkEpisode(
)
/**
* Network representation of [EpisodeEntity] when fetched from /networkepisodes{id}
* Network representation of [Episode] when fetched from /episodes/{id}
*/
@Serializable
data class NetworkEpisodeExpanded(
@PrimaryKey
val id: Int,
val name: String,
@Serializable(InstantSerializer::class)
@ -52,19 +50,3 @@ data class NetworkEpisodeExpanded(
val newsResources: List<NetworkNewsResource> = listOf(),
val authors: List<NetworkAuthor> = listOf(),
)
fun NetworkEpisode.asEntity() = EpisodeEntity(
id = id,
name = name,
publishDate = publishDate,
alternateVideo = alternateVideo,
alternateAudio = alternateAudio,
)
fun NetworkEpisodeExpanded.asEntity() = EpisodeEntity(
id = id,
name = name,
publishDate = publishDate,
alternateVideo = alternateVideo,
alternateAudio = alternateAudio,
)

@ -14,14 +14,17 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.network
package com.google.samples.apps.nowinandroid.core.network.model
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.network.model.util.InstantSerializer
import com.google.samples.apps.nowinandroid.core.network.model.util.NewsResourceTypeSerializer
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Network representation of [NewsResourceEntity] when fetched from /networkresources
* Network representation of [NewsResource] when fetched from /newsresources
*/
@Serializable
data class NetworkNewsResource(
@ -32,13 +35,14 @@ data class NetworkNewsResource(
val url: String,
@Serializable(InstantSerializer::class)
val publishDate: Instant,
val type: String,
@Serializable(NewsResourceTypeSerializer::class)
val type: NewsResourceType,
val authors: List<Int> = listOf(),
val topics: List<Int> = listOf(),
)
/**
* Network representation of [NewsResourceEntity] when fetched from /networkresources{id}
* Network representation of [NewsResource] when fetched from /newsresources/{id}
*/
@Serializable
data class NetworkNewsResourceExpanded(
@ -49,27 +53,8 @@ data class NetworkNewsResourceExpanded(
val url: String,
@Serializable(InstantSerializer::class)
val publishDate: Instant,
val type: String,
@Serializable(NewsResourceTypeSerializer::class)
val type: NewsResourceType,
val authors: List<NetworkAuthor> = listOf(),
val topics: List<NetworkTopic> = listOf(),
)
fun NetworkNewsResource.asEntity() = NewsResourceEntity(
id = id,
episodeId = episodeId,
title = title,
content = content,
url = url,
publishDate = publishDate,
type = type,
)
fun NetworkNewsResourceExpanded.asEntity() = NewsResourceEntity(
id = id,
episodeId = episodeId,
title = title,
content = content,
url = url,
publishDate = publishDate,
type = type,
)

@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.network
package com.google.samples.apps.nowinandroid.core.network.model
import com.google.samples.apps.nowinandroid.core.model.entities.TopicEntity
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import kotlinx.serialization.Serializable
/**
* Network representation of [TopicEntity]
* Network representation of [Topic]
*/
@Serializable
data class NetworkTopic(
@ -29,10 +29,3 @@ data class NetworkTopic(
val description: String = "",
val followed: Boolean = false,
)
fun NetworkTopic.asEntity() = TopicEntity(
id = id,
name = name,
description = description,
followed = followed
)

@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.model.network
package com.google.samples.apps.nowinandroid.core.network.model.util
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant

@ -0,0 +1,39 @@
/*
* 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.util
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType
import com.google.samples.apps.nowinandroid.core.model.data.asNewsResourceType
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind.STRING
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
object NewsResourceTypeSerializer : KSerializer<NewsResourceType> {
override fun deserialize(decoder: Decoder): NewsResourceType =
decoder.decodeString().asNewsResourceType()
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
serialName = "type",
kind = STRING
)
override fun serialize(encoder: Encoder, value: NewsResourceType) =
encoder.encodeString(value.name)
}

@ -37,11 +37,10 @@ import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.model.data.Author
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.core.model.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.core.model.entities.NewsResourceEntity
import com.google.samples.apps.nowinandroid.core.model.entities.TopicEntity
import com.google.samples.apps.nowinandroid.core.model.data.NewsResourceType.Article
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import com.google.samples.apps.nowinandroid.core.ui.theme.NiaTheme
import kotlinx.datetime.Instant
@ -59,12 +58,12 @@ fun NewsResourceCardExpanded(
modifier = Modifier.padding(16.dp)
) {
Row {
NewsResourceTitle(newsResource.entity.title, modifier = Modifier.fillMaxWidth((.8f)))
NewsResourceTitle(newsResource.title, modifier = Modifier.fillMaxWidth((.8f)))
Spacer(modifier = Modifier.weight(1f))
BookmarkButton(isBookmarked, onToggleBookmark)
}
Spacer(modifier = Modifier.height(12.dp))
NewsResourceShortDescription(newsResource.entity.content)
NewsResourceShortDescription(newsResource.content)
}
}
@ -174,31 +173,22 @@ fun ExpandedNewsResourcePreview() {
}
private val newsResource = NewsResource(
NewsResourceEntity(
id = 1,
episodeId = 1,
title = "Title",
content = "Content",
url = "url",
publishDate = Instant.DISTANT_FUTURE,
type = "type",
),
EpisodeEntity(
id = 1,
name = "Title",
publishDate = Instant.DISTANT_FUTURE,
alternateVideo = "alternateVideo",
alternateAudio = "alternateAudio",
),
listOf(
AuthorEntity(
id = 1,
episodeId = 1,
title = "Title",
content = "Content",
url = "url",
publishDate = Instant.DISTANT_FUTURE,
type = Article,
authors = listOf(
Author(
id = 1,
name = "Name",
imageUrl = "imageUrl"
)
),
listOf(
TopicEntity(
topics = listOf(
Topic(
id = 1,
name = "Name",
description = "Description",

@ -145,6 +145,7 @@ private val testOutputTopics = listOf(
Topic(
id = 2,
name = TOPIC_3_NAME,
description = TOPIC_DESC
description = TOPIC_DESC,
followed = false,
)
)

@ -109,7 +109,7 @@ class ForYouViewModelTest {
Topic(
id = 2,
name = "Tools",
description = ""
description = "",
) to false
),
feed = emptyList()

Loading…
Cancel
Save