From c75f7ed025fdbd5a0ca42f52d377cb86bb53b405 Mon Sep 17 00:00:00 2001
From: Adetunji Dahunsi <dahunsi@google.com>
Date: Tue, 1 Feb 2022 17:03:48 -0500
Subject: [PATCH] Add entity relationships and defined network deserialization
 strategy

Change-Id: I239cdc28237a87a0ed6599892e8ac7c61776a46d
---
 app/build.gradle                              |   4 +
 .../ui/foryou/ForYouScreenTest.kt             |   2 +-
 .../data/{news => }/fake/FakeData.kt          | 649 +++---------------
 .../{news => }/fake/FakeNewsRepository.kt     |  31 +-
 .../nowinandroid/data/fake/FakeNiANetwork.kt  |  46 ++
 .../{news => }/fake/FakeTopicsRepository.kt   |  17 +-
 .../nowinandroid/data/local/NiADatabase.kt    |  49 ++
 .../data/local/entities/AuthorEntity.kt       |  40 ++
 .../local/entities/EpisodeAuthorCrossRef.kt   |  49 ++
 .../data/local/entities/EpisodeEntity.kt      |  41 ++
 .../entities/NewsResourceAuthorCrossRef.kt    |  49 ++
 .../data/local/entities/NewsResourceEntity.kt |  51 ++
 .../entities/NewsResourceTopicCrossRef.kt     |  49 ++
 .../data/local/entities/TopicEntity.kt        |  38 +
 .../data/local/utilities/Converters.kt        |  45 ++
 .../apps/nowinandroid/data/model/Episode.kt   |  44 ++
 .../nowinandroid/data/model/NewsResource.kt   |  52 ++
 .../data/model/NewsResourceType.kt            |  63 ++
 .../data/{news => model}/Topic.kt             |  10 +-
 .../data/network/NetworkAuthor.kt             |  36 +
 .../data/network/NetworkEpisode.kt            |  71 ++
 .../data/network/NetworkNewsResource.kt       |  76 ++
 .../nowinandroid/data/network/NetworkTopic.kt |  36 +
 .../nowinandroid/data/network/NiANetwork.kt   |  24 +
 .../utilities/InstantSerializer.kt}           |  39 +-
 .../{news => repository}/NewsRepository.kt    |   3 +-
 .../{news => repository}/TopicsRepository.kt  |   5 +-
 .../samples/apps/nowinandroid/di/AppModule.kt |  15 +-
 .../apps/nowinandroid/ui/NewsResourceCard.kt  |   4 +-
 .../nowinandroid/ui/foryou/ForYouScreen.kt    |   4 +-
 .../nowinandroid/ui/foryou/ForYouViewModel.kt |   8 +-
 .../data/fake/FakeNewsRepositoryTest.kt       |  35 +
 .../FakeNiANetworkTest.kt}                    |  13 +-
 .../data/network/NetworkEntityKtTest.kt       | 129 ++++
 .../testutil/TestNewsRepository.kt            |   4 +-
 .../testutil/TestTopicsRepository.kt          |   4 +-
 .../ui/foryou/ForYouViewModelTest.kt          |   2 +-
 build.gradle                                  |   2 +-
 gradle/libs.versions.toml                     |   5 +
 settings.gradle                               |   1 +
 40 files changed, 1194 insertions(+), 651 deletions(-)
 rename app/src/main/java/com/google/samples/apps/nowinandroid/data/{news => }/fake/FakeData.kt (82%)
 rename app/src/main/java/com/google/samples/apps/nowinandroid/data/{news => }/fake/FakeNewsRepository.kt (58%)
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeNiANetwork.kt
 rename app/src/main/java/com/google/samples/apps/nowinandroid/data/{news => }/fake/FakeTopicsRepository.kt (73%)
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/NiADatabase.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/AuthorEntity.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeAuthorCrossRef.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeEntity.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceAuthorCrossRef.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceEntity.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceTopicCrossRef.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/TopicEntity.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/local/utilities/Converters.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/model/Episode.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResource.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResourceType.kt
 rename app/src/main/java/com/google/samples/apps/nowinandroid/data/{news => model}/Topic.kt (79%)
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkAuthor.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkEpisode.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkNewsResource.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkTopic.kt
 create mode 100644 app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NiANetwork.kt
 rename app/src/main/java/com/google/samples/apps/nowinandroid/data/{news/NewsResource.kt => network/utilities/InstantSerializer.kt} (58%)
 rename app/src/main/java/com/google/samples/apps/nowinandroid/data/{news => repository}/NewsRepository.kt (88%)
 rename app/src/main/java/com/google/samples/apps/nowinandroid/data/{news => repository}/TopicsRepository.kt (85%)
 create mode 100644 app/src/test/java/com/google/samples/apps/nowinandroid/data/fake/FakeNewsRepositoryTest.kt
 rename app/src/test/java/com/google/samples/apps/nowinandroid/data/{news/fake/FakeNewsRepositoryTest.kt => fake/FakeNiANetworkTest.kt} (80%)
 create mode 100644 app/src/test/java/com/google/samples/apps/nowinandroid/data/network/NetworkEntityKtTest.kt

diff --git a/app/build.gradle b/app/build.gradle
index 8966e4a0d..58c8c465a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
diff --git a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreenTest.kt b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreenTest.kt
index 3f67709e6..adb0706e3 100644
--- a/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreenTest.kt
+++ b/app/src/androidTest/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreenTest.kt
@@ -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
 
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeData.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeData.kt
similarity index 82%
rename from app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeData.kt
rename to app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeData.kt
index 664283435..c6e97f921 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeData.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeData.kt
@@ -14,17 +14,16 @@
  * 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.NewsResource
-import com.google.samples.apps.nowinandroid.data.news.VideoInfo
+import com.google.samples.apps.nowinandroid.data.network.NetworkNewsResource
 import kotlinx.datetime.LocalDateTime
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.toInstant
 import org.intellij.lang.annotations.Language
 
 object FakeDataSource {
-    val sampleResource = NewsResource(
+    val sampleResource = NetworkNewsResource(
         id = 1,
         episodeId = 52,
         title = "Thanks for helping us reach 1M YouTube Subscribers",
@@ -42,11 +41,6 @@ object FakeDataSource {
         ).toInstant(TimeZone.UTC),
         type = "Video \uD83D\uDCFA",
         topics = listOf(0),
-        alternateVideo = VideoInfo(
-            url = "",
-            startTimestamp = 0,
-            endTimestamp = 0
-        )
     )
 
     @Language("JSON")
@@ -165,12 +159,7 @@ object FakeDataSource {
       "topics": [
         0
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 2,
@@ -185,12 +174,7 @@ object FakeDataSource {
       ],
       "authors": [
         0
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 3,
@@ -203,12 +187,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 4,
@@ -221,12 +200,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 5,
@@ -241,12 +215,7 @@ object FakeDataSource {
       ],
       "authors": [
         1
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 6,
@@ -261,12 +230,7 @@ object FakeDataSource {
       ],
       "authors": [
         1
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 7,
@@ -281,12 +245,7 @@ object FakeDataSource {
       ],
       "authors": [
         1
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 8,
@@ -301,12 +260,7 @@ object FakeDataSource {
       ],
       "authors": [
         1
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 9,
@@ -320,12 +274,7 @@ object FakeDataSource {
         6,
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 10,
@@ -339,12 +288,7 @@ object FakeDataSource {
         6,
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 11,
@@ -358,12 +302,7 @@ object FakeDataSource {
         6,
         14
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 12,
@@ -377,12 +316,7 @@ object FakeDataSource {
         6,
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 13,
@@ -396,12 +330,7 @@ object FakeDataSource {
         6,
         18
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 14,
@@ -417,12 +346,7 @@ object FakeDataSource {
       ],
       "authors": [
         2
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 15,
@@ -438,12 +362,7 @@ object FakeDataSource {
       ],
       "authors": [
         3
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 16,
@@ -459,12 +378,7 @@ object FakeDataSource {
       ],
       "authors": [
         4
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 17,
@@ -477,12 +391,7 @@ object FakeDataSource {
       "topics": [
         3
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 18,
@@ -497,12 +406,7 @@ object FakeDataSource {
       ],
       "authors": [
         5
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 19,
@@ -517,12 +421,7 @@ object FakeDataSource {
       ],
       "authors": [
         5
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 20,
@@ -538,12 +437,7 @@ object FakeDataSource {
       ],
       "authors": [
         6
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 21,
@@ -560,12 +454,7 @@ object FakeDataSource {
       ],
       "authors": [
         7
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 22,
@@ -578,12 +467,7 @@ object FakeDataSource {
       "topics": [
         10
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 23,
@@ -596,12 +480,7 @@ object FakeDataSource {
       "topics": [
         10
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 24,
@@ -616,12 +495,7 @@ object FakeDataSource {
       ],
       "authors": [
         8
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 25,
@@ -636,12 +510,7 @@ object FakeDataSource {
       ],
       "authors": [
         9
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 26,
@@ -655,12 +524,7 @@ object FakeDataSource {
         10,
         11
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 27,
@@ -673,12 +537,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 28,
@@ -691,12 +550,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 29,
@@ -709,12 +563,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 30,
@@ -728,12 +577,7 @@ object FakeDataSource {
         1,
         4
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 31,
@@ -746,12 +590,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 32,
@@ -765,12 +604,7 @@ object FakeDataSource {
         1,
         13
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 33,
@@ -784,12 +618,7 @@ object FakeDataSource {
         6,
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 34,
@@ -802,12 +631,7 @@ object FakeDataSource {
       "topics": [
         13
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 35,
@@ -820,12 +644,7 @@ object FakeDataSource {
       "topics": [
         13
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 36,
@@ -838,12 +657,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 37,
@@ -858,12 +672,7 @@ object FakeDataSource {
       ],
       "authors": [
         10
-      ],
-      "alternateVideo": {
-        "URL": "https://storage.googleapis.com/now-in-android/NIA049.mp4",
-        "startTimestamp": 82,
-        "endTimestamp": 97
-      }
+      ]
     },
     {
       "id": 38,
@@ -876,12 +685,7 @@ object FakeDataSource {
       "topics": [
         14
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "https://storage.googleapis.com/now-in-android/NIA049.mp4",
-        "startTimestamp": 97,
-        "endTimestamp": 121
-      }
+      "authors": []
     },
     {
       "id": 39,
@@ -894,12 +698,7 @@ object FakeDataSource {
       "topics": [
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "https://storage.googleapis.com/now-in-android/NIA049.mp4",
-        "startTimestamp": 193,
-        "endTimestamp": 206
-      }
+      "authors": []
     },
     {
       "id": 40,
@@ -913,12 +712,7 @@ object FakeDataSource {
         12,
         5
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "https://storage.googleapis.com/now-in-android/NIA049.mp4",
-        "startTimestamp": 206,
-        "endTimestamp": 220
-      }
+      "authors": []
     },
     {
       "id": 41,
@@ -931,12 +725,7 @@ object FakeDataSource {
       "topics": [
         9
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 42,
@@ -949,12 +738,7 @@ object FakeDataSource {
       "topics": [
         9
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 43,
@@ -968,12 +752,7 @@ object FakeDataSource {
         9,
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 44,
@@ -987,12 +766,7 @@ object FakeDataSource {
         9,
         1
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 45,
@@ -1005,12 +779,7 @@ object FakeDataSource {
       "topics": [
         9
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 46,
@@ -1026,12 +795,7 @@ object FakeDataSource {
       ],
       "authors": [
         0
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 47,
@@ -1047,12 +811,7 @@ object FakeDataSource {
       ],
       "authors": [
         11
-      ],
-      "alternateVideo": {
-        "URL": "https://storage.googleapis.com/now-in-android/NIA049.mp4",
-        "startTimestamp": 49,
-        "endTimestamp": 82
-      }
+      ]
     },
     {
       "id": 48,
@@ -1067,12 +826,7 @@ object FakeDataSource {
       ],
       "authors": [
         7
-      ],
-      "alternateVideo": {
-        "URL": "https://storage.googleapis.com/now-in-android/NIA049.mp4",
-        "startTimestamp": 233,
-        "endTimestamp": 287
-      }
+      ]
     },
     {
       "id": 49,
@@ -1087,12 +841,7 @@ object FakeDataSource {
       ],
       "authors": [
         5
-      ],
-      "alternateVideo": {
-        "URL": "https://storage.googleapis.com/now-in-android/NIA049.mp4",
-        "startTimestamp": 136,
-        "endTimestamp": 193
-      }
+      ]
     },
     {
       "id": 50,
@@ -1107,12 +856,7 @@ object FakeDataSource {
       ],
       "authors": [
         12
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 51,
@@ -1128,12 +872,7 @@ object FakeDataSource {
       ],
       "authors": [
         6
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 52,
@@ -1148,12 +887,7 @@ object FakeDataSource {
       ],
       "authors": [
         0
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 53,
@@ -1168,12 +902,7 @@ object FakeDataSource {
       ],
       "authors": [
         13
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 54,
@@ -1188,12 +917,7 @@ object FakeDataSource {
       ],
       "authors": [
         14
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 55,
@@ -1209,12 +933,7 @@ object FakeDataSource {
       ],
       "authors": [
         6
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 56,
@@ -1227,12 +946,7 @@ object FakeDataSource {
       "topics": [
         0
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 57,
@@ -1247,12 +961,7 @@ object FakeDataSource {
       ],
       "authors": [
         15
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 58,
@@ -1267,12 +976,7 @@ object FakeDataSource {
       ],
       "authors": [
         16
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 59,
@@ -1287,12 +991,7 @@ object FakeDataSource {
       ],
       "authors": [
         17
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 60,
@@ -1307,12 +1006,7 @@ object FakeDataSource {
       ],
       "authors": [
         18
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 61,
@@ -1327,12 +1021,7 @@ object FakeDataSource {
       ],
       "authors": [
         19
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 62,
@@ -1347,12 +1036,7 @@ object FakeDataSource {
       ],
       "authors": [
         20
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 63,
@@ -1367,12 +1051,7 @@ object FakeDataSource {
       ],
       "authors": [
         21
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 64,
@@ -1387,12 +1066,7 @@ object FakeDataSource {
       ],
       "authors": [
         5
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 65,
@@ -1407,12 +1081,7 @@ object FakeDataSource {
       ],
       "authors": [
         22
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 66,
@@ -1425,12 +1094,7 @@ object FakeDataSource {
       "topics": [
         15
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 67,
@@ -1443,12 +1107,7 @@ object FakeDataSource {
       "topics": [
         6
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 68,
@@ -1464,12 +1123,7 @@ object FakeDataSource {
       ],
       "authors": [
         23
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 69,
@@ -1484,12 +1138,7 @@ object FakeDataSource {
       ],
       "authors": [
         7
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 70,
@@ -1502,12 +1151,7 @@ object FakeDataSource {
       "topics": [
         7
       ],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 71,
@@ -1522,12 +1166,7 @@ object FakeDataSource {
       ],
       "authors": [
         24
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 72,
@@ -1543,12 +1182,7 @@ object FakeDataSource {
       ],
       "authors": [
         25
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 73,
@@ -1563,12 +1197,7 @@ object FakeDataSource {
       ],
       "authors": [
         11
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 74,
@@ -1584,12 +1213,7 @@ object FakeDataSource {
       ],
       "authors": [
         26
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 75,
@@ -1605,12 +1229,7 @@ object FakeDataSource {
       ],
       "authors": [
         27
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 76,
@@ -1621,12 +1240,7 @@ object FakeDataSource {
       "publishDate": "2021-06-29T23:00:00.000Z",
       "type": "Jetpack release 🚀",
       "topics": [],
-      "authors": [],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      "authors": []
     },
     {
       "id": 77,
@@ -1642,12 +1256,7 @@ object FakeDataSource {
       ],
       "authors": [
         28
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 78,
@@ -1662,12 +1271,7 @@ object FakeDataSource {
       ],
       "authors": [
         15
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 79,
@@ -1683,12 +1287,7 @@ object FakeDataSource {
       ],
       "authors": [
         29
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 80,
@@ -1703,12 +1302,7 @@ object FakeDataSource {
       ],
       "authors": [
         10
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 81,
@@ -1723,12 +1317,7 @@ object FakeDataSource {
       ],
       "authors": [
         10
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 82,
@@ -1743,12 +1332,7 @@ object FakeDataSource {
       ],
       "authors": [
         30
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 83,
@@ -1763,12 +1347,7 @@ object FakeDataSource {
       ],
       "authors": [
         7
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 84,
@@ -1783,12 +1362,7 @@ object FakeDataSource {
       ],
       "authors": [
         31
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 85,
@@ -1803,12 +1377,7 @@ object FakeDataSource {
       ],
       "authors": [
         32
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 86,
@@ -1824,12 +1393,7 @@ object FakeDataSource {
       ],
       "authors": [
         33
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 87,
@@ -1844,12 +1408,7 @@ object FakeDataSource {
       ],
       "authors": [
         8
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 88,
@@ -1864,12 +1423,7 @@ object FakeDataSource {
       ],
       "authors": [
         34
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 89,
@@ -1884,12 +1438,7 @@ object FakeDataSource {
       ],
       "authors": [
         7
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 90,
@@ -1904,12 +1453,7 @@ object FakeDataSource {
       ],
       "authors": [
         23
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     },
     {
       "id": 91,
@@ -1924,12 +1468,7 @@ object FakeDataSource {
       ],
       "authors": [
         35
-      ],
-      "alternateVideo": {
-        "URL": "",
-        "startTimestamp": 0,
-        "endTimestamp": 0
-      }
+      ]
     }
   ]
 }
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeNewsRepository.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeNewsRepository.kt
similarity index 58%
rename from app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeNewsRepository.kt
rename to app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeNewsRepository.kt
index 883231e8a..12f116d85 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeNewsRepository.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeNewsRepository.kt
@@ -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>
-)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeNiANetwork.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeNiANetwork.kt
new file mode 100644
index 000000000..d9339fce0
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeNiANetwork.kt
@@ -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>
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeTopicsRepository.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeTopicsRepository.kt
similarity index 73%
rename from app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeTopicsRepository.kt
rename to app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeTopicsRepository.kt
index 81217e3fc..2a83d2cac 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeTopicsRepository.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/fake/FakeTopicsRepository.kt
@@ -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)
 
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/NiADatabase.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/NiADatabase.kt
new file mode 100644
index 000000000..e312b0567
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/NiADatabase.kt
@@ -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()
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/AuthorEntity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/AuthorEntity.kt
new file mode 100644
index 000000000..735fb129a
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/AuthorEntity.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeAuthorCrossRef.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeAuthorCrossRef.kt
new file mode 100644
index 000000000..52212a393
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeAuthorCrossRef.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeEntity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeEntity.kt
new file mode 100644
index 000000000..bf6a7722f
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/EpisodeEntity.kt
@@ -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?,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceAuthorCrossRef.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceAuthorCrossRef.kt
new file mode 100644
index 000000000..10b17f8cf
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceAuthorCrossRef.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceEntity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceEntity.kt
new file mode 100644
index 000000000..0c0bb938d
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceEntity.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceTopicCrossRef.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceTopicCrossRef.kt
new file mode 100644
index 000000000..3955a035e
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/NewsResourceTopicCrossRef.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/TopicEntity.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/TopicEntity.kt
new file mode 100644
index 000000000..d683ece36
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/entities/TopicEntity.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/utilities/Converters.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/utilities/Converters.kt
new file mode 100644
index 000000000..25580530e
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/local/utilities/Converters.kt
@@ -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
+    }
+}
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/Episode.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/Episode.kt
new file mode 100644
index 000000000..964d072e6
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/Episode.kt
@@ -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>
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResource.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResource.kt
new file mode 100644
index 000000000..e6ff75498
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResource.kt
@@ -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>
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResourceType.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResourceType.kt
new file mode 100644
index 000000000..4e10b2e52
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/NewsResourceType.kt
@@ -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"
+    )
+}
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/Topic.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/Topic.kt
similarity index 79%
rename from app/src/main/java/com/google/samples/apps/nowinandroid/data/news/Topic.kt
rename to app/src/main/java/com/google/samples/apps/nowinandroid/data/model/Topic.kt
index 45dc13967..0bd5f1f70 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/Topic.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/model/Topic.kt
@@ -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,
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkAuthor.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkAuthor.kt
new file mode 100644
index 000000000..b66d97719
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkAuthor.kt
@@ -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
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkEpisode.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkEpisode.kt
new file mode 100644
index 000000000..924167a53
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkEpisode.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkNewsResource.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkNewsResource.kt
new file mode 100644
index 000000000..89ed61cea
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkNewsResource.kt
@@ -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,
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkTopic.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkTopic.kt
new file mode 100644
index 000000000..208720b57
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NetworkTopic.kt
@@ -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
+)
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NiANetwork.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NiANetwork.kt
new file mode 100644
index 000000000..2e2903051
--- /dev/null
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/NiANetwork.kt
@@ -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>
+}
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/NewsResource.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/utilities/InstantSerializer.kt
similarity index 58%
rename from app/src/main/java/com/google/samples/apps/nowinandroid/data/news/NewsResource.kt
rename to app/src/main/java/com/google/samples/apps/nowinandroid/data/network/utilities/InstantSerializer.kt
index e3657abc2..fa8d17373 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/NewsResource.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/network/utilities/InstantSerializer.kt
@@ -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) =
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/NewsRepository.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/repository/NewsRepository.kt
similarity index 88%
rename from app/src/main/java/com/google/samples/apps/nowinandroid/data/news/NewsRepository.kt
rename to app/src/main/java/com/google/samples/apps/nowinandroid/data/repository/NewsRepository.kt
index 44e8db2fc..973604b1e 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/NewsRepository.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/repository/NewsRepository.kt
@@ -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
 
 /**
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/TopicsRepository.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/data/repository/TopicsRepository.kt
similarity index 85%
rename from app/src/main/java/com/google/samples/apps/nowinandroid/data/news/TopicsRepository.kt
rename to app/src/main/java/com/google/samples/apps/nowinandroid/data/repository/TopicsRepository.kt
index 687a93df2..80d1ded81 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/data/news/TopicsRepository.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/data/repository/TopicsRepository.kt
@@ -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 {
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/di/AppModule.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/di/AppModule.kt
index 406d2bb19..2f5a4c997 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/di/AppModule.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/di/AppModule.kt
@@ -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
 
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NewsResourceCard.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NewsResourceCard.kt
index 96001c9d4..2ed94d520 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NewsResourceCard.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/NewsResourceCard.kt
@@ -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
  */
 
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreen.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreen.kt
index 62782f8f8..0034517da 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreen.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouScreen.kt
@@ -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(
diff --git a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModel.kt b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModel.kt
index d48362f7a..49e88c47c 100644
--- a/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModel.kt
+++ b/app/src/main/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModel.kt
@@ -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
diff --git a/app/src/test/java/com/google/samples/apps/nowinandroid/data/fake/FakeNewsRepositoryTest.kt b/app/src/test/java/com/google/samples/apps/nowinandroid/data/fake/FakeNewsRepositoryTest.kt
new file mode 100644
index 000000000..7f0cc42cf
--- /dev/null
+++ b/app/src/test/java/com/google/samples/apps/nowinandroid/data/fake/FakeNewsRepositoryTest.kt
@@ -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 }
+        )
+    }
+}
diff --git a/app/src/test/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeNewsRepositoryTest.kt b/app/src/test/java/com/google/samples/apps/nowinandroid/data/fake/FakeNiANetworkTest.kt
similarity index 80%
rename from app/src/test/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeNewsRepositoryTest.kt
rename to app/src/test/java/com/google/samples/apps/nowinandroid/data/fake/FakeNiANetworkTest.kt
index e1ca56857..bc66f226c 100644
--- a/app/src/test/java/com/google/samples/apps/nowinandroid/data/news/fake/FakeNewsRepositoryTest.kt
+++ b/app/src/test/java/com/google/samples/apps/nowinandroid/data/fake/FakeNiANetworkTest.kt
@@ -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()
         )
     }
 }
diff --git a/app/src/test/java/com/google/samples/apps/nowinandroid/data/network/NetworkEntityKtTest.kt b/app/src/test/java/com/google/samples/apps/nowinandroid/data/network/NetworkEntityKtTest.kt
new file mode 100644
index 000000000..81e8a2ce9
--- /dev/null
+++ b/app/src/test/java/com/google/samples/apps/nowinandroid/data/network/NetworkEntityKtTest.kt
@@ -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)
+    }
+}
diff --git a/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestNewsRepository.kt b/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestNewsRepository.kt
index e1908ecce..c47db0d91 100644
--- a/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestNewsRepository.kt
+++ b/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestNewsRepository.kt
@@ -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
diff --git a/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestTopicsRepository.kt b/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestTopicsRepository.kt
index 4150fe405..bb60d885f 100644
--- a/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestTopicsRepository.kt
+++ b/app/src/test/java/com/google/samples/apps/nowinandroid/testutil/TestTopicsRepository.kt
@@ -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
diff --git a/app/src/test/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModelTest.kt b/app/src/test/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModelTest.kt
index 2dc80c5b1..2e56e1d58 100644
--- a/app/src/test/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModelTest.kt
+++ b/app/src/test/java/com/google/samples/apps/nowinandroid/ui/foryou/ForYouViewModelTest.kt
@@ -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
diff --git a/build.gradle b/build.gradle
index b2d8f4fa8..fcc2bdb2c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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')
         }
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a85970c79..f256673ff 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }
diff --git a/settings.gradle b/settings.gradle
index 676708897..e24e3409c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -23,6 +23,7 @@ dependencyResolutionManagement {
     repositories {
         google()
         mavenCentral()
+        gradlePluginPortal()
     }
 }
 rootProject.name = "nowinandroid"