Skip to content

Commit

Permalink
FeedDataSource implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ychescale9 committed Mar 19, 2024
1 parent 7bf3cc9 commit 4b673a8
Show file tree
Hide file tree
Showing 10 changed files with 952 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,8 @@ upsertFeedItem:
INSERT OR REPLACE INTO feedItemEntity
VALUES (:id, :feed_origin_key, :title, :publish_time, :content_url, :image_url, :description, :issue_number, :podcast_audio_url, :podcast_duration, :podcast_start_position, :saved_for_later);

updatePotcastStartPositionById:
UPDATE feedItemEntity SET podcast_start_position = :podcast_start_position WHERE id = :id;

updateSavedForLaterById:
UPDATE feedItemEntity SET saved_for_later = :savedForLater WHERE id = :id;
UPDATE feedItemEntity SET saved_for_later = :saved_for_later WHERE id = :id;
1 change: 1 addition & 0 deletions kmp/feed-datasource/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ kotlin {
implementation(project(":kmp:networking:testing"))
implementation(project(":kmp:database-testing"))
implementation(libs.kotlinx.coroutines.test)
implementation(libs.turbine)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,118 +1,106 @@
package io.github.reactivecircus.kstreamlined.kmp.feed.datasource

import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import app.cash.sqldelight.coroutines.mapToOneOrNull
import io.github.reactivecircus.kstreamlined.kmp.database.KStreamlinedDatabase
import io.github.reactivecircus.kstreamlined.kmp.feed.datasource.mapper.asExternalModel
import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedItem
import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedOrigin
import io.github.reactivecircus.kstreamlined.kmp.model.feed.KotlinWeeklyIssueItem
import io.github.reactivecircus.kstreamlined.kmp.networking.FeedService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.datetime.toInstant
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext

public class FeedDataSource(
private val feedService: FeedService,
private val database: KStreamlinedDatabase,
private val db: KStreamlinedDatabase,
private val dbDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
public suspend fun selectFeedSource(feedOrigin: FeedOrigin) {
feedOrigin.selected
TODO()
}

public suspend fun unselectFeedSource(feedOrigin: FeedOrigin) {
feedOrigin.selected
TODO()
}

@Suppress("MaxLineLength")
@OptIn(ExperimentalCoroutinesApi::class)
public fun streamFeedItemById(id: String): Flow<FeedItem?> {
database.feedItemEntityQueries
return savedItemsIds.mapLatest { savedItemsIds ->
when {
id.contains("kotlin-weekly") -> {
FeedItem.KotlinWeekly(
id = id,
title = "Kotlin Weekly #386",
publishTime = "2023-11-05T08:13:58Z".toInstant(),
contentUrl = id,
savedForLater = savedItemsIds.contains(id),
issueNumber = 386,
)
}

id.contains("tag:soundcloud") -> fakeTalkingKotlinEpisode.copy(
id = id,
savedForLater = savedItemsIds.contains(id)
)

id.contains("blog.jetbrains.com") -> FeedItem.KotlinBlog(
id = id,
title = "Tackle Advent of Code 2023 With Kotlin and Win Prizes!",
publishTime = "2023-11-23T17:00:38Z".toInstant(),
contentUrl = "https://blog.jetbrains.com/kotlin/2023/11/advent-of-code-2023-with-kotlin/",
savedForLater = savedItemsIds.contains(id),
featuredImageUrl = "https://blog.jetbrains.com/wp-content/uploads/2023/11/DSGN-18072-Social-media-banners_Blog-Featured-image-1280x720-1.png",
)

id.contains("yt:video") -> FeedItem.KotlinYouTube(
id = id,
title = "Coil Goes Multiplatform with Colin White | Talking Kotlin #127",
publishTime = "2023-11-29T17:30:08Z".toInstant(),
contentUrl = "https://www.youtube.com/watch?v=apiVJfLvUBE",
savedForLater = savedItemsIds.contains(id),
thumbnailUrl = "https://i2.ytimg.com/vi/apiVJfLvUBE/hqdefault.jpg",
description = "Welcome to another engaging episode of Talking Kotlin! In this edition, we dive into the dynamic world of Android development with Colin White, the creator of the widely acclaimed Coil library. Join us as we discuss the latest developments, insights, and the exciting roadmap for Coil. \uD83D\uDE80 Highlights from this Episode: Learn about Colin's journey in developing the Coil library. Discover the pivotal role Coil plays in simplifying image loading for Android developers. Get an exclusive sneak peek into the upcoming Coil 3.0, featuring multi-platform support and seamless integration with Jetpack Compose. \uD83D\uDD17 Helpful Links: Coil Library GitHub: https://coil-kt.github.io/coil/ Follow Colin White on Twitter: https://twitter.com/colinwhi \uD83C\uDF10 Connect with the Kotlin Community: https://kotlinlang.org/community/ Kotlin Foundation: https://kotlinfoundation.org/ \uD83D\uDC49 Don't miss out on the latest insights and updates from the Kotlin world! Subscribe, hit the bell icon, and join the conversation in the comments below. \uD83D\uDCC8 Help us reach 20,000 views by liking, sharing, and subscribing! Your support keeps the Kotlin conversation alive.",
)

else -> null
public fun streamFeedOrigins(): Flow<List<FeedOrigin>> {
return db.feedOriginEntityQueries.allFeedOrigins()
.asFlow().mapToList(dbDispatcher)
.map { origins ->
origins.map { it.asExternalModel() }
}
}.distinctUntilChanged()
}

public suspend fun addSavedFeedItem(id: String) {
savedItemsIds.value += id
public fun streamFeedItemsForSelectedOrigins(): Flow<List<FeedItem>> {
return db.feedItemEntityQueries.feedItemsForSelectedOrigins()
.asFlow().mapToList(dbDispatcher)
.map { items ->
items.map { it.asExternalModel() }
}
}

public suspend fun removeSavedFeedItem(id: String) {
savedItemsIds.value -= id
public fun streamSavedFeedItems(): Flow<List<FeedItem>> {
return db.feedItemEntityQueries.savedFeedItems()
.asFlow().mapToList(dbDispatcher)
.map { items ->
items.map { it.asExternalModel() }
}
}

public suspend fun loadSavedFeedItems(): List<FeedItem> {
TODO()
public fun streamFeedItemById(id: String): Flow<FeedItem?> {
return db.feedItemEntityQueries.feedItemById(id)
.asFlow().mapToOneOrNull(dbDispatcher)
.map { it?.asExternalModel() }
}

public suspend fun loadKotlinWeeklyIssue(url: String): List<KotlinWeeklyIssueItem> {
// TODO persist fetched entry to DB
return feedService.fetchKotlinWeeklyIssue(url).map {
it.asExternalModel()
}
}

public suspend fun saveTalkingKotlinEpisodeStartPosition(id: String, positionMillis: Long) {
// TODO persist to DB
fakeTalkingKotlinEpisode = fakeTalkingKotlinEpisode.copy(
id = id,
startPositionMillis = positionMillis,
)
public suspend fun selectFeedSource(feedOriginKey: FeedOrigin.Key): Unit =
withContext(dbDispatcher) {
db.feedOriginEntityQueries.updateSelection(selected = true, key = feedOriginKey.name)
}

public suspend fun unselectFeedSource(feedOriginKey: FeedOrigin.Key): Unit =
withContext(dbDispatcher) {
db.transaction {
// select all other origins if the one being unselected is the last one selected
val currentFeedOrigins = db.feedOriginEntityQueries.allFeedOrigins().executeAsList()
val selectedOrigins = currentFeedOrigins.filter { it.selected }
val isLastSelected = selectedOrigins.size == 1 &&
selectedOrigins.first().key == feedOriginKey.name
if (isLastSelected) {
currentFeedOrigins.filter { it.key != feedOriginKey.name }.forEach { origin ->
db.feedOriginEntityQueries.updateSelection(
selected = true,
key = origin.key,
)
}
}
db.feedOriginEntityQueries.updateSelection(
selected = false,
key = feedOriginKey.name
)
}
}

public suspend fun addSavedFeedItem(id: String): Unit = withContext(dbDispatcher) {
db.feedItemEntityQueries.updateSavedForLaterById(saved_for_later = true, id = id)
}

private val savedItemsIds = MutableStateFlow(emptySet<String>())
public suspend fun removeSavedFeedItem(id: String): Unit = withContext(dbDispatcher) {
db.feedItemEntityQueries.updateSavedForLaterById(saved_for_later = false, id = id)
}

@Suppress("MaxLineLength")
private var fakeTalkingKotlinEpisode = FeedItem.TalkingKotlin(
id = "tag:soundcloud,2010:tracks/1689535512",
title = "Coil Goes Multiplatform with Colin White",
publishTime = "2023-11-29T22:00:00Z".toInstant(),
contentUrl = "https://soundcloud.com/user-38099918/coil-goes-multiplatform-with-colin-white",
savedForLater = false,
audioUrl = "https://feeds.soundcloud.com/stream/1689535512-user-38099918-network-resilient-applications-with-store5-talking-kotlin-128.mp3",
thumbnailUrl = "https://talkingkotlin.com/images/kotlin_talking_logo.png",
summary = "Welcome to another engaging episode of Talking Kotlin! In this edition, we dive into the dynamic world of Android development with Colin White, the creator of the widely acclaimed Coil library. Join us as we discuss the latest developments, insights, and the exciting roadmap for Coil. \uD83D\uDE80 Highlights from this Episode: Learn about Colin's journey in developing the Coil library. Discover the pivotal role Coil plays in simplifying image loading for Android developers. Get an exclusive sneak peek into the upcoming Coil 3.0, featuring multi-platform support and seamless integration with Jetpack Compose. \uD83D\uDD17 Helpful Links: Coil Library GitHub: github.com/coilkt/coil Follow Colin White on Twitter: @colinwhi \uD83C\uDF10 Connect with the Kotlin Community: https://kotlinlang.org/community/ Kotlin Foundation: https://kotlinfoundation.org/",
duration = "42min.",
startPositionMillis = 0,
)
public suspend fun saveTalkingKotlinEpisodeStartPosition(
id: String,
positionMillis: Long,
): Unit = withContext(dbDispatcher) {
db.feedItemEntityQueries.updatePotcastStartPositionById(
id = id,
podcast_start_position = positionMillis,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.github.reactivecircus.kstreamlined.kmp.feed.datasource.mapper

import io.github.reactivecircus.kstreamlined.kmp.database.FeedItemEntity
import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedItem
import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedOrigin

internal fun FeedItemEntity.asExternalModel(): FeedItem {
return when (feed_origin_key) {
FeedOrigin.Key.KotlinBlog.name -> FeedItem.KotlinBlog(
id = id,
title = title,
publishTime = publish_time,
contentUrl = content_url,
savedForLater = saved_for_later,
featuredImageUrl = image_url!!,
)

FeedOrigin.Key.KotlinYouTubeChannel.name -> FeedItem.KotlinYouTube(
id = id,
title = title,
publishTime = publish_time,
contentUrl = content_url,
savedForLater = saved_for_later,
thumbnailUrl = image_url!!,
description = description!!,
)

FeedOrigin.Key.TalkingKotlinPodcast.name -> FeedItem.TalkingKotlin(
id = id,
title = title,
publishTime = publish_time,
contentUrl = content_url,
savedForLater = saved_for_later,
audioUrl = podcast_audio_url!!,
thumbnailUrl = image_url!!,
summary = description!!,
duration = podcast_duration!!,
startPositionMillis = podcast_start_position ?: 0,
)

FeedOrigin.Key.KotlinWeekly.name -> FeedItem.KotlinWeekly(
id = id,
title = title,
publishTime = publish_time,
contentUrl = content_url,
savedForLater = saved_for_later,
issueNumber = issue_number!!.toInt(),
)

else -> error("Unknown feed origin key: $feed_origin_key")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.github.reactivecircus.kstreamlined.kmp.feed.datasource.mapper

import io.github.reactivecircus.kstreamlined.kmp.database.FeedOriginEntity
import io.github.reactivecircus.kstreamlined.kmp.model.feed.FeedOrigin

internal fun FeedOriginEntity.asExternalModel(): FeedOrigin {
return FeedOrigin(
key = when (key) {
FeedOrigin.Key.KotlinBlog.name -> FeedOrigin.Key.KotlinBlog
FeedOrigin.Key.KotlinYouTubeChannel.name -> FeedOrigin.Key.KotlinYouTubeChannel
FeedOrigin.Key.TalkingKotlinPodcast.name -> FeedOrigin.Key.TalkingKotlinPodcast
FeedOrigin.Key.KotlinWeekly.name -> FeedOrigin.Key.KotlinWeekly
else -> error("Unknown feed origin key: $key")
},
title = title,
description = description,
selected = selected,
)
}
Loading

0 comments on commit 4b673a8

Please sign in to comment.