Add entity relationships and defined network deserialization strategy

Change-Id: I239cdc28237a87a0ed6599892e8ac7c61776a46d
pull/2/head
Adetunji Dahunsi 3 years ago
parent 632bc62ed5
commit c75f7ed025

@ -21,6 +21,7 @@ plugins {
id 'jacoco'
id 'dagger.hilt.android.plugin'
alias(libs.plugins.protobuf)
alias(libs.plugins.ksp)
}
def jacocoTestReport = tasks.create("jacocoTestReport")
@ -161,6 +162,9 @@ dependencies {
implementation libs.hilt.android
kapt libs.hilt.compiler
implementation libs.room.runtime
ksp libs.room.compiler
implementation libs.protobuf.kotlin.lite
debugImplementation libs.androidx.compose.ui.testManifest

@ -25,7 +25,7 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.data.news.Topic
import com.google.samples.apps.nowinandroid.data.model.Topic
import org.junit.Rule
import org.junit.Test

@ -14,18 +14,14 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.data.news.fake
package com.google.samples.apps.nowinandroid.data.fake
import com.google.samples.apps.nowinandroid.data.news.NewsRepository
import com.google.samples.apps.nowinandroid.data.news.NewsResource
import com.google.samples.apps.nowinandroid.data.model.NewsResource
import com.google.samples.apps.nowinandroid.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.di.NiaDispatchers
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.coroutines.flow.flowOf
import kotlinx.serialization.json.Json
/**
@ -36,21 +32,10 @@ class FakeNewsRepository @Inject constructor(
private val dispatchers: NiaDispatchers,
private val networkJson: Json
) : NewsRepository {
override fun getNewsResourcesStream(): Flow<List<NewsResource>> = flow {
emit(networkJson.decodeFromString<ResourceData>(FakeDataSource.data).resources)
}
.flowOn(dispatchers.IO)
override fun getNewsResourcesStream(): Flow<List<NewsResource>> =
flowOf(emptyList())
override fun getNewsResourcesStream(filterTopicIds: Set<Int>): Flow<List<NewsResource>> =
getNewsResourcesStream().map { newsResources ->
newsResources.filter { it.topics.intersect(filterTopicIds.toSet()).isNotEmpty() }
}
flowOf(emptyList())
}
/**
* Representation of resources aas fetched from [FakeDataSource]
*/
@Serializable
private data class ResourceData(
val resources: List<NewsResource>
)

@ -0,0 +1,46 @@
/*
* Copyright 2021 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.data.fake
import com.google.samples.apps.nowinandroid.data.network.NetworkNewsResource
import com.google.samples.apps.nowinandroid.data.network.NiANetwork
import com.google.samples.apps.nowinandroid.di.NiaDispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
/**
* [NiANetwork] implementation that provides static news resources to aid development
*/
class FakeNiANetwork(
private val dispatchers: NiaDispatchers,
private val networkJson: Json
) : NiANetwork {
override suspend fun getNewsResources(): List<NetworkNewsResource> =
withContext(dispatchers.IO) {
networkJson.decodeFromString<ResourceData>(FakeDataSource.data).resources
}
}
/**
* Representation of resources as fetched from [FakeDataSource]
*/
@Serializable
private data class ResourceData(
val resources: List<NetworkNewsResource>
)

@ -14,11 +14,12 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.data.news.fake
package com.google.samples.apps.nowinandroid.data.fake
import com.google.samples.apps.nowinandroid.data.NiaPreferences
import com.google.samples.apps.nowinandroid.data.news.Topic
import com.google.samples.apps.nowinandroid.data.news.TopicsRepository
import com.google.samples.apps.nowinandroid.data.model.Topic
import com.google.samples.apps.nowinandroid.data.network.NetworkTopic
import com.google.samples.apps.nowinandroid.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.di.NiaDispatchers
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@ -33,7 +34,15 @@ class FakeTopicsRepository @Inject constructor(
private val niaPreferences: NiaPreferences
) : TopicsRepository {
override fun getTopicsStream(): Flow<List<Topic>> = flow<List<Topic>> {
emit(networkJson.decodeFromString(FakeDataSource.topicsData))
emit(
networkJson.decodeFromString<List<NetworkTopic>>(FakeDataSource.topicsData).map {
Topic(
id = it.id,
name = it.name,
description = it.description
)
}
)
}
.flowOn(dispatchers.IO)

@ -0,0 +1,49 @@
/*
* Copyright 2021 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.data.local
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.google.samples.apps.nowinandroid.data.local.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.data.local.entities.EpisodeAuthorCrossRef
import com.google.samples.apps.nowinandroid.data.local.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceAuthorCrossRef
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceEntity
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.data.local.entities.TopicEntity
import com.google.samples.apps.nowinandroid.data.local.utilities.InstantConverter
import com.google.samples.apps.nowinandroid.data.local.utilities.NewsResourceTypeConverter
// TODO: ADD DAOs
@Database(
entities = [
AuthorEntity::class,
EpisodeAuthorCrossRef::class,
EpisodeEntity::class,
NewsResourceAuthorCrossRef::class,
NewsResourceEntity::class,
NewsResourceTopicCrossRef::class,
TopicEntity::class,
],
version = 1,
)
@TypeConverters(
InstantConverter::class,
NewsResourceTypeConverter::class,
)
abstract class NiADatabase : RoomDatabase()

@ -0,0 +1,40 @@
/*
* Copyright 2021 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.data.local.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
* It has a many to many relationship with both entities
*/
@Entity(
tableName = "authors",
indices = [
Index(value = ["name"], unique = true)
],
)
data class AuthorEntity(
@PrimaryKey
val id: Int,
val name: String,
@ColumnInfo(name = "image_url")
val imageUrl: String,
)

@ -0,0 +1,49 @@
/*
* Copyright 2021 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.data.local.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
/**
* Cross reference for many to many relationship between [EpisodeEntity] and [AuthorEntity]
*/
@Entity(
tableName = "episodes_authors",
primaryKeys = ["episode_id", "author_id"],
foreignKeys = [
ForeignKey(
entity = EpisodeEntity::class,
parentColumns = ["id"],
childColumns = ["episode_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = AuthorEntity::class,
parentColumns = ["id"],
childColumns = ["author_id"],
onDelete = ForeignKey.CASCADE
),
]
)
data class EpisodeAuthorCrossRef(
@ColumnInfo(name = "episode_id")
val episodeId: Int,
@ColumnInfo(name = "author_id")
val authorId: Long,
)

@ -0,0 +1,41 @@
/*
* Copyright 2021 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.data.local.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.datetime.Instant
/**
* Defines an NiA episode.
* It is a parent in a 1 to many relationship with [NewsResourceEntity]
*/
@Entity(
tableName = "episodes",
)
data class EpisodeEntity(
@PrimaryKey
val id: Int,
val name: String,
@ColumnInfo(name = "publish_date")
val publishDate: Instant,
@ColumnInfo(name = "alternate_video")
val alternateVideo: String?,
@ColumnInfo(name = "alternate_audio")
val alternateAudio: String?,
)

@ -0,0 +1,49 @@
/*
* Copyright 2021 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.data.local.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
/**
* Cross reference for many to many relationship between [NewsResourceEntity] and [AuthorEntity]
*/
@Entity(
tableName = "news_resources_authors",
primaryKeys = ["news_resource_id", "author_id"],
foreignKeys = [
ForeignKey(
entity = NewsResourceEntity::class,
parentColumns = ["id"],
childColumns = ["news_resource_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = AuthorEntity::class,
parentColumns = ["id"],
childColumns = ["author_id"],
onDelete = ForeignKey.CASCADE
),
]
)
data class NewsResourceAuthorCrossRef(
@ColumnInfo(name = "news_resource_id")
val newsResourceId: Int,
@ColumnInfo(name = "author_id")
val authorId: Long,
)

@ -0,0 +1,51 @@
/*
* Copyright 2021 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.data.local.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import kotlinx.datetime.Instant
/**
* Defines an NiA news resource.
* It is the child in a 1 to many relationship with [EpisodeEntity]
*/
@Entity(
tableName = "news_resources",
foreignKeys = [
ForeignKey(
entity = EpisodeEntity::class,
parentColumns = ["id"],
childColumns = ["episode_id"],
onDelete = ForeignKey.CASCADE
),
]
)
data class NewsResourceEntity(
@PrimaryKey
val id: Int,
@ColumnInfo(name = "episode_id")
val episodeId: Int,
val title: String,
val content: String,
val url: String,
@ColumnInfo(name = "publish_date")
val publishDate: Instant,
val type: String,
)

@ -0,0 +1,49 @@
/*
* Copyright 2021 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.data.local.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
/**
* Cross reference for many to many relationship between [NewsResourceEntity] and [TopicEntity]
*/
@Entity(
tableName = "news_resources_topics",
primaryKeys = ["news_resource_id", "topic_id"],
foreignKeys = [
ForeignKey(
entity = NewsResourceEntity::class,
parentColumns = ["id"],
childColumns = ["news_resource_id"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = TopicEntity::class,
parentColumns = ["id"],
childColumns = ["topic_id"],
onDelete = ForeignKey.CASCADE
),
]
)
data class NewsResourceTopicCrossRef(
@ColumnInfo(name = "news_resource_id")
val newsResourceId: Int,
@ColumnInfo(name = "topic_id")
val topicId: Int,
)

@ -0,0 +1,38 @@
/*
* Copyright 2021 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.data.local.entities
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
/**
* Defines a topic a user may follow.
* It has a many to many relationship with [NewsResourceEntity]
*/
@Entity(
tableName = "topics",
indices = [
Index(value = ["name"], unique = true)
]
)
data class TopicEntity(
@PrimaryKey
val id: Int,
val name: String,
val description: String,
)

@ -0,0 +1,45 @@
/*
* Copyright 2021 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.data.local.utilities
import androidx.room.TypeConverter
import com.google.samples.apps.nowinandroid.data.model.NewsResourceType
import kotlinx.datetime.Instant
class InstantConverter {
@TypeConverter
fun longToInstant(value: Long?): Instant? =
value?.let(Instant::fromEpochMilliseconds)
@TypeConverter
fun instantToLong(instant: Instant?): Long? =
instant?.toEpochMilliseconds()
}
class NewsResourceTypeConverter {
@TypeConverter
fun newsResourceTypeToString(value: NewsResourceType?): String? =
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
}
}

@ -0,0 +1,44 @@
/*
* Copyright 2021 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.data.model
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import com.google.samples.apps.nowinandroid.data.local.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.data.local.entities.EpisodeAuthorCrossRef
import com.google.samples.apps.nowinandroid.data.local.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceEntity
/**
* 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 = "episode_id",
entityColumn = "author_id",
associateBy = Junction(EpisodeAuthorCrossRef::class)
)
val authors: List<AuthorEntity>
)

@ -0,0 +1,52 @@
/*
* Copyright 2021 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.data.model
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
import com.google.samples.apps.nowinandroid.data.local.entities.AuthorEntity
import com.google.samples.apps.nowinandroid.data.local.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceAuthorCrossRef
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceEntity
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceTopicCrossRef
import com.google.samples.apps.nowinandroid.data.local.entities.TopicEntity
/**
* 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 = "news_resource_id",
entityColumn = "author_id",
associateBy = Junction(NewsResourceAuthorCrossRef::class)
)
val authors: List<AuthorEntity>,
@Relation(
parentColumn = "news_resource_id",
entityColumn = "topic_id",
associateBy = Junction(NewsResourceTopicCrossRef::class)
)
val topics: List<TopicEntity>
)

@ -0,0 +1,63 @@
/*
* Copyright 2021 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.data.model
/**
* Type for [NewsResource]
*/
enum class NewsResourceType(
val displayText: String,
// TODO: descriptions should probably be string resources
val description: String
) {
Video(
displayText = "Video 📺",
description = "A video published on YouTube"
),
APIChange(
displayText = "API change",
description = "An addition, deprecation or change to the Android platform APIs."
),
Article(
displayText = "Article 📚",
description = "An article, typically on Medium or the official Android blog"
),
Codelab(
displayText = "Codelab",
description = "A new or updated codelab"
),
Podcast(
displayText = "Podcast 🎙",
description = "A podcast"
),
Docs(
displayText = "Docs 📑",
description = "A new or updated piece of documentation"
),
Event(
displayText = "Event 📆",
description = "Information about a developer event e.g. Android Developer Summit"
),
DAC(
displayText = "DAC",
description = "Android version features - Information about features in an Android"
),
Unknown(
displayText = "Unknown",
description = "Unknown"
)
}

@ -1,5 +1,5 @@
/*
* Copyright 2022 The Android Open Source Project
* Copyright 2021 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.
@ -14,11 +14,11 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.data.news
package com.google.samples.apps.nowinandroid.data.model
import kotlinx.serialization.Serializable
@Serializable
/**
* External data layer representation of a NiA Topic
*/
data class Topic(
val id: Int,
val name: String,

@ -0,0 +1,36 @@
/*
* Copyright 2021 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.data.network
import com.google.samples.apps.nowinandroid.data.local.entities.AuthorEntity
import kotlinx.serialization.Serializable
/**
* Network representation of [AuthorEntity]
*/
@Serializable
data class NetworkAuthor(
val id: Int,
val name: String,
val imageUrl: String,
)
fun NetworkAuthor.asEntity() = AuthorEntity(
id = id,
name = name,
imageUrl = imageUrl
)

@ -0,0 +1,71 @@
/*
* Copyright 2021 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.data.network
import androidx.room.PrimaryKey
import com.google.samples.apps.nowinandroid.data.local.entities.EpisodeEntity
import com.google.samples.apps.nowinandroid.data.network.utilities.InstantSerializer
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Network representation of [EpisodeEntity] when fetched from /networkepisodes
*/
@Serializable
data class NetworkEpisode(
@PrimaryKey
val id: Int,
val name: String,
@Serializable(InstantSerializer::class)
val publishDate: Instant,
val alternateVideo: String?,
val alternateAudio: String?,
val newsResources: List<Int> = listOf(),
val authors: List<Int> = listOf(),
)
/**
* Network representation of [EpisodeEntity] when fetched from /networkepisodes{id}
*/
@Serializable
data class NetworkEpisodeExpanded(
@PrimaryKey
val id: Int,
val name: String,
@Serializable(InstantSerializer::class)
val publishDate: Instant,
val alternateVideo: String,
val alternateAudio: String,
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,
)

@ -0,0 +1,76 @@
/*
* Copyright 2021 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.data.network
import com.google.samples.apps.nowinandroid.data.local.entities.NewsResourceEntity
import com.google.samples.apps.nowinandroid.data.network.utilities.InstantSerializer
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
/**
* Network representation of [NewsResourceEntity] when fetched from /networkresources
*/
@Serializable
data class NetworkNewsResource(
val id: Int,
val episodeId: Int,
val title: String,
val content: String,
val url: String,
@Serializable(InstantSerializer::class)
val publishDate: Instant,
val type: String,
val authors: List<Int> = listOf(),
val topics: List<Int> = listOf(),
)
/**
* Network representation of [NewsResourceEntity] when fetched from /networkresources{id}
*/
@Serializable
data class NetworkNewsResourceExpanded(
val id: Int,
val episodeId: Int,
val title: String,
val content: String,
val url: String,
@Serializable(InstantSerializer::class)
val publishDate: Instant,
val type: String,
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,
)

@ -0,0 +1,36 @@
/*
* Copyright 2021 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.data.network
import com.google.samples.apps.nowinandroid.data.local.entities.TopicEntity
import kotlinx.serialization.Serializable
/**
* Network representation of [TopicEntity]
*/
@Serializable
data class NetworkTopic(
val id: Int,
val name: String = "",
val description: String = "",
)
fun NetworkTopic.asEntity() = TopicEntity(
id = id,
name = name,
description = description
)

@ -0,0 +1,24 @@
/*
* Copyright 2021 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.data.network
/**
* Interface representing network calls to the NIA backend
*/
interface NiANetwork {
suspend fun getNewsResources(): List<NetworkNewsResource>
}

@ -14,55 +14,24 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.data.news
package com.google.samples.apps.nowinandroid.data.network.utilities
import kotlinx.datetime.Instant
import kotlinx.datetime.toInstant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
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
/**
* Item representing a summary of a noteworthy item from a Now In Android episode
*/
@Serializable
data class NewsResource(
val id: Int,
val episodeId: Int,
val title: String,
val content: String,
val url: String,
val authors: List<Int>,
@Serializable(InstantSerializer::class)
val publishDate: Instant,
val type: String,
val topics: List<Int>,
val alternateVideo: VideoInfo?
)
/**
* Data class summarizing video metadata
*/
@Serializable
data class VideoInfo(
@SerialName("URL")
val url: String,
val startTimestamp: Int,
val endTimestamp: Int,
)
private object InstantSerializer : KSerializer<Instant> {
object InstantSerializer : KSerializer<Instant> {
override fun deserialize(decoder: Decoder): Instant =
decoder.decodeString().toInstant()
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(
serialName = "Instant",
kind = PrimitiveKind.STRING
kind = STRING
)
override fun serialize(encoder: Encoder, value: Instant) =

@ -14,8 +14,9 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.data.news
package com.google.samples.apps.nowinandroid.data.repository
import com.google.samples.apps.nowinandroid.data.model.NewsResource
import kotlinx.coroutines.flow.Flow
/**

@ -1,5 +1,5 @@
/*
* Copyright 2022 The Android Open Source Project
* Copyright 2021 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.
@ -14,8 +14,9 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.data.news
package com.google.samples.apps.nowinandroid.data.repository
import com.google.samples.apps.nowinandroid.data.model.Topic
import kotlinx.coroutines.flow.Flow
interface TopicsRepository {

@ -22,10 +22,12 @@ import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.google.samples.apps.nowinandroid.data.UserPreferences
import com.google.samples.apps.nowinandroid.data.UserPreferencesSerializer
import com.google.samples.apps.nowinandroid.data.news.NewsRepository
import com.google.samples.apps.nowinandroid.data.news.TopicsRepository
import com.google.samples.apps.nowinandroid.data.news.fake.FakeNewsRepository
import com.google.samples.apps.nowinandroid.data.news.fake.FakeTopicsRepository
import com.google.samples.apps.nowinandroid.data.fake.FakeNewsRepository
import com.google.samples.apps.nowinandroid.data.fake.FakeNiANetwork
import com.google.samples.apps.nowinandroid.data.fake.FakeTopicsRepository
import com.google.samples.apps.nowinandroid.data.network.NiANetwork
import com.google.samples.apps.nowinandroid.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.data.repository.TopicsRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -39,6 +41,11 @@ import kotlinx.serialization.json.Json
@InstallIn(SingletonComponent::class)
interface AppModule {
@Binds
fun bindsNiANetwork(
fakeNiANetwork: FakeNiANetwork
): NiANetwork
@Binds
fun bindsTopicRepository(fakeTopicsRepository: FakeTopicsRepository): TopicsRepository

@ -30,11 +30,11 @@ import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.data.news.NewsResource
import com.google.samples.apps.nowinandroid.data.model.NewsResource
import com.google.samples.apps.nowinandroid.ui.theme.NiaTheme
/**
* [com.google.samples.apps.nowinandroid.data.news.NewsResource] card used on the following screens:
* [com.google.samples.apps.nowinandroid.data.model.NewsResource] card used on the following screens:
* For You, Episodes, Saved
*/

@ -44,8 +44,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.flowlayout.FlowRow
import com.google.samples.apps.nowinandroid.R
import com.google.samples.apps.nowinandroid.data.news.NewsResource
import com.google.samples.apps.nowinandroid.data.news.Topic
import com.google.samples.apps.nowinandroid.data.model.NewsResource
import com.google.samples.apps.nowinandroid.data.model.Topic
@Composable
fun ForYouRoute(

@ -22,10 +22,10 @@ import androidx.compose.runtime.snapshots.Snapshot.Companion.withMutableSnapshot
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.data.news.NewsRepository
import com.google.samples.apps.nowinandroid.data.news.NewsResource
import com.google.samples.apps.nowinandroid.data.news.Topic
import com.google.samples.apps.nowinandroid.data.news.TopicsRepository
import com.google.samples.apps.nowinandroid.data.model.NewsResource
import com.google.samples.apps.nowinandroid.data.model.Topic
import com.google.samples.apps.nowinandroid.data.repository.NewsRepository
import com.google.samples.apps.nowinandroid.data.repository.TopicsRepository
import com.google.samples.apps.nowinandroid.ui.saveable
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@ -0,0 +1,35 @@
/*
* Copyright 2021 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.data.fake
import com.google.samples.apps.nowinandroid.di.DefaultNiaDispatchers
import kotlinx.serialization.json.Json
import org.junit.Before
class FakeNewsRepositoryTest {
private lateinit var subject: FakeNewsRepository
@Before
fun setup() {
subject = FakeNewsRepository(
// TODO: Create test-specific NiaDispatchers
dispatchers = DefaultNiaDispatchers(),
networkJson = Json { ignoreUnknownKeys = true }
)
}
}

@ -14,23 +14,22 @@
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.data.news.fake
package com.google.samples.apps.nowinandroid.data.fake
import com.google.samples.apps.nowinandroid.di.DefaultNiaDispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
class FakeNewsRepositoryTest {
class FakeNiANetworkTest {
private lateinit var subject: FakeNewsRepository
private lateinit var subject: FakeNiANetwork
@Before
fun setup() {
subject = FakeNewsRepository(
fun setUp() {
subject = FakeNiANetwork(
// TODO: Create test-specific NiaDispatchers
dispatchers = DefaultNiaDispatchers(),
networkJson = Json { ignoreUnknownKeys = true }
@ -41,7 +40,7 @@ class FakeNewsRepositoryTest {
fun testDeserializationOfNewsResources() = runTest {
assertEquals(
FakeDataSource.sampleResource,
subject.getNewsResourcesStream().first().first()
subject.getNewsResources().first()
)
}
}

@ -0,0 +1,129 @@
/*
* Copyright 2021 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.data.network
import com.google.samples.apps.nowinandroid.data.model.NewsResourceType
import kotlinx.datetime.Instant
import org.junit.Assert.assertEquals
import org.junit.Test
class NetworkEntityKtTest {
@Test
fun network_author_can_be_mapped_to_author_entity() {
val networkModel = NetworkAuthor(
id = 0,
name = "Test",
imageUrl = "something"
)
val entity = networkModel.asEntity()
assertEquals(0, entity.id)
assertEquals("Test", entity.name)
assertEquals("something", entity.imageUrl)
}
@Test
fun network_topic_can_be_mapped_to_topic_entity() {
val networkModel = NetworkTopic(
id = 0,
name = "Test",
description = "something"
)
val entity = networkModel.asEntity()
assertEquals(0, entity.id)
assertEquals("Test", entity.name)
assertEquals("something", entity.description)
}
@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 entity = networkModel.asEntity()
assertEquals(0, entity.id)
assertEquals(2, entity.episodeId)
assertEquals("title", entity.title)
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,
)
val entityFromExpanded = expandedNetworkModel.asEntity()
assertEquals(0, entityFromExpanded.id)
assertEquals(2, entityFromExpanded.episodeId)
assertEquals("title", entityFromExpanded.title)
assertEquals("content", entityFromExpanded.content)
assertEquals("url", entityFromExpanded.url)
assertEquals(Instant.fromEpochMilliseconds(1), entityFromExpanded.publishDate)
assertEquals(NewsResourceType.Article.displayText, entityFromExpanded.type)
}
@Test
fun network_episode_can_be_mapped_to_episode_entity() {
val networkModel = NetworkEpisode(
id = 0,
name = "name",
publishDate = Instant.fromEpochMilliseconds(1),
alternateVideo = "alternateVideo",
alternateAudio = "alternateAudio",
)
val entity = networkModel.asEntity()
assertEquals(0, entity.id)
assertEquals("name", entity.name)
assertEquals("alternateVideo", entity.alternateVideo)
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 entityFromExpanded = expandedNetworkModel.asEntity()
assertEquals(0, entityFromExpanded.id)
assertEquals("name", entityFromExpanded.name)
assertEquals("alternateVideo", entityFromExpanded.alternateVideo)
assertEquals("alternateAudio", entityFromExpanded.alternateAudio)
assertEquals(Instant.fromEpochMilliseconds(1), entityFromExpanded.publishDate)
}
}

@ -16,8 +16,8 @@
package com.google.samples.apps.nowinandroid.testutil
import com.google.samples.apps.nowinandroid.data.news.NewsRepository
import com.google.samples.apps.nowinandroid.data.news.NewsResource
import com.google.samples.apps.nowinandroid.data.model.NewsResource
import com.google.samples.apps.nowinandroid.data.repository.NewsRepository
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow

@ -16,8 +16,8 @@
package com.google.samples.apps.nowinandroid.testutil
import com.google.samples.apps.nowinandroid.data.news.Topic
import com.google.samples.apps.nowinandroid.data.news.TopicsRepository
import com.google.samples.apps.nowinandroid.data.model.Topic
import com.google.samples.apps.nowinandroid.data.repository.TopicsRepository
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow

@ -18,7 +18,7 @@ package com.google.samples.apps.nowinandroid.ui.foryou
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.google.samples.apps.nowinandroid.data.news.Topic
import com.google.samples.apps.nowinandroid.data.model.Topic
import com.google.samples.apps.nowinandroid.testutil.TestDispatcherRule
import com.google.samples.apps.nowinandroid.testutil.TestNewsRepository
import com.google.samples.apps.nowinandroid.testutil.TestTopicsRepository

@ -49,7 +49,7 @@ subprojects {
target '**/*.kt'
targetExclude("$buildDir/**/*.kt")
targetExclude('bin/**/*.kt')
targetExclude("$rootDir/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeData.kt")
targetExclude("$rootDir/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeData.kt")
ktlint(libs.versions.ktlint.get()).userData([android: "true"])
licenseHeaderFile rootProject.file('spotless/copyright.kt')
}

@ -22,11 +22,13 @@ kotlinxCoroutines = "1.6.0"
kotlinxCoroutinesTest = "1.6.0"
kotlinxDatetime = "0.3.1"
kotlinxSerializationJson = "1.3.1"
ksp = "1.6.0-1.0.1"
ktlint = "0.43.0"
material3 = "1.5.0-alpha05"
mockk = "1.12.1"
protobuf = "3.19.1"
protobufPlugin = "0.8.18"
room = "2.4.1"
spotless = "6.0.0"
turbine = "0.7.0"
@ -71,7 +73,10 @@ mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
protobuf-protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
[plugins]
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

@ -23,6 +23,7 @@ dependencyResolutionManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "nowinandroid"

Loading…
Cancel
Save