Skip to content

Commit

Permalink
Move to SQLDelight for offline tag & bookmark caching
Browse files Browse the repository at this point in the history
  • Loading branch information
danielyrovas committed Apr 16, 2024
1 parent b57775c commit 5ae7d3d
Show file tree
Hide file tree
Showing 23 changed files with 724 additions and 244 deletions.
14 changes: 12 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl

@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
plugins {
alias(libs.plugins.android)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.sqldelight)
}

sqldelight {
databases {
create("Database") {
packageName.set("org.yrovas.linklater")
}
}
}

android {
Expand Down Expand Up @@ -84,12 +92,14 @@ dependencies {
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)

implementation(libs.coroutines.extensions)
implementation(libs.android.driver)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(platform(libs.compose.bom))
androidTestImplementation(libs.ui.test.junit4)
debugImplementation(libs.ui.tooling)
debugImplementation(libs.ui.test.manifest)
implementation(kotlin("script-runtime"))
}
37 changes: 34 additions & 3 deletions app/src/main/java/org/yrovas/linklater/AppComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import androidx.sqlite.db.SupportSQLiteDatabase
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.DefaultRequest
Expand All @@ -20,8 +23,12 @@ import kotlinx.serialization.json.Json
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
import me.tatarka.inject.annotations.Scope
import org.yrovas.linklater.data.local.BookmarkDataSource
import org.yrovas.linklater.data.local.BookmarkDataSourceImpl
import org.yrovas.linklater.data.local.PrefDataStore
import org.yrovas.linklater.data.local.PrefStore
import org.yrovas.linklater.data.local.TagDataSource
import org.yrovas.linklater.data.local.TagDataSourceImpl
import org.yrovas.linklater.data.remote.BookmarkAPI
import org.yrovas.linklater.data.remote.LinkDingAPI
import org.yrovas.linklater.ui.activity.DestinationHost
Expand All @@ -39,11 +46,12 @@ abstract class AppComponent(
abstract val destinationHost: DestinationHost
abstract val prefStore: PrefDataStore

private val store: DataStore<Preferences>
get() = context.dataStore
val store: DataStore<Preferences>
@Provides get() = context.dataStore

@Provides
fun providePrefStore(): PrefDataStore = PrefStore(store)
fun providePrefStore(store: DataStore<Preferences>): PrefDataStore =
PrefStore(store)

@Provides
fun provideHttpClient(): HttpClient = HttpClient(Android) {
Expand All @@ -70,4 +78,27 @@ abstract class AppComponent(
abstract val linkDingAPI: LinkDingAPI
val bookmarkAPI: BookmarkAPI
@Provides get() = linkDingAPI

abstract val bookmarkDataSource: BookmarkDataSource

@Provides
fun provideBookmarkDataSource(db: Database): BookmarkDataSource =
BookmarkDataSourceImpl(db)

@Provides
fun provideTagDataSource(db: Database): TagDataSource = TagDataSourceImpl(db)

@Provides
fun provideSQLDriver(context: Context): SqlDriver =
AndroidSqliteDriver(schema = Database.Schema,
context = context,
name = "linklater.db",
callback = object : AndroidSqliteDriver.Callback(Database.Schema) {
override fun onOpen(db: SupportSQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
}
})

@Provides
fun provideDB(driver: SqlDriver): Database = Database(driver)
}
7 changes: 3 additions & 4 deletions app/src/main/java/org/yrovas/linklater/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ fun Context.getAppVersion(): String {
}
}

fun Context.toast(text: String, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, text, length).show()
}

@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
annotation class ThemePreview
Expand All @@ -96,3 +92,6 @@ fun String?.isNull(): Boolean {
fun String?.isNotNull(): Boolean {
return this != null
}

fun String.intoTags(): List<String> =
split(" ").filter { it.isNotBlank() }.distinct()
93 changes: 57 additions & 36 deletions app/src/main/java/org/yrovas/linklater/data/Bookmark.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ package org.yrovas.linklater.data

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import linklater.GetBookmarkByIDwithTags
import linklater.GetBookmarkByURLwithTags
import linklater.GetBookmarksWithTags

// A 1:1 representation of a LinkDing bookmark.
@Serializable
data class Bookmark(
val id: Int,
val id: Long,
val url: String,
val title: String? = null,
val description: String? = null,
Expand All @@ -21,38 +24,56 @@ data class Bookmark(
@SerialName("tag_names") val tags: List<String> = emptyList(),
)

// A datatype representing bookmarks which will be sent to the remote API.
@Serializable
data class LocalBookmark(
var url: String,
var title: String? = null,
var description: String? = null,
var notes: String? = null,
var is_archived: Boolean = false,
var unread: Boolean = false,
var shared: Boolean = false,
@SerialName("tag_names") var tags: List<String> = emptyList(),
) {
/// Returns a new local bookmark with the new values
fun withUpdates(
url: String? = null,
title: String? = null,
description: String? = null,
notes: String? = null,
is_archived: Boolean? = null,
unread: Boolean? = null,
shared: Boolean? = null,
tags: List<String>? = null,
): LocalBookmark {
return LocalBookmark(
url = url?.ifBlank { null } ?: url ?: this.url,
title = title?.ifBlank { null } ?: title ?: this.title,
description = description?.ifBlank { null } ?: description ?: this.description,
notes = notes?.ifBlank { null } ?: notes ?: this.notes,
is_archived = is_archived ?: this.is_archived,
unread = unread ?: this.unread,
shared = shared ?: this.shared,
tags = tags ?: this.tags
)
}
}
fun GetBookmarksWithTags.toBookmark() = Bookmark(
id = id,
url = url,
title = title,
description = description,
notes = notes,
website_title = website_title,
website_description = website_description,
is_archived = is_archived ?: false,
unread = unread ?: false,
shared = shared ?: false,
date_added = date_added,
date_modified = date_modified,
tags = tagNames.tryIntoTagList()
)

fun GetBookmarkByIDwithTags.toBookmark() = Bookmark(
id = id,
url = url,
title = title,
description = description,
notes = notes,
website_title = website_title,
website_description = website_description,
is_archived = is_archived ?: false,
unread = unread ?: false,
shared = shared ?: false,
date_added = date_added,
date_modified = date_modified,
tags = tagNames.tryIntoTagList()
)

fun GetBookmarkByURLwithTags.toBookmark() = Bookmark(
id = id,
url = url,
title = title,
description = description,
notes = notes,
website_title = website_title,
website_description = website_description,
is_archived = is_archived ?: false,
unread = unread ?: false,
shared = shared ?: false,
date_added = date_added,
date_modified = date_modified,
tags = tagNames.tryIntoTagList()
)

private fun String?.tryIntoTagList(): List<String> =
this?.intoTagList() ?: emptyList()

private fun String.intoTagList(): List<String> =
this.split(":: ::").filter { it.isNotBlank() }
40 changes: 40 additions & 0 deletions app/src/main/java/org/yrovas/linklater/data/LocalBookmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.yrovas.linklater.data

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

// A datatype representing bookmarks which will be sent to the remote API.
@Serializable
data class LocalBookmark(
var url: String,
var title: String? = null,
var description: String? = null,
var notes: String? = null,
var is_archived: Boolean = false,
var unread: Boolean = false,
var shared: Boolean = false,
@SerialName("tag_names") var tags: List<String> = emptyList(),
) {
/// Returns a new local bookmark with the new values
fun withUpdates(
url: String? = null,
title: String? = null,
description: String? = null,
notes: String? = null,
is_archived: Boolean? = null,
unread: Boolean? = null,
shared: Boolean? = null,
tags: List<String>? = null,
): LocalBookmark {
return LocalBookmark(
url = url?.ifBlank { null } ?: url ?: this.url,
title = title?.ifBlank { null } ?: title ?: this.title,
description = description?.ifBlank { null } ?: description ?: this.description,
notes = notes?.ifBlank { null } ?: notes ?: this.notes,
is_archived = is_archived ?: this.is_archived,
unread = unread ?: this.unread,
shared = shared ?: this.shared,
tags = tags ?: this.tags
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.yrovas.linklater.data.local

import kotlinx.coroutines.flow.Flow
import org.yrovas.linklater.data.Bookmark

interface BookmarkDataSource {
suspend fun getBookmark(id: Long): Bookmark?
fun getBookmarks(): Flow<List<Bookmark>>
suspend fun insertBookmark(bookmark: Bookmark)
suspend fun insertBookmarks(bookmarks: List<Bookmark>)
suspend fun deleteBookmark(id: Long)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.yrovas.linklater.data.local

import android.util.Log
import app.cash.sqldelight.coroutines.asFlow
import app.cash.sqldelight.coroutines.mapToList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.yrovas.linklater.Database
import org.yrovas.linklater.data.Bookmark
import org.yrovas.linklater.data.toBookmark

const val TAG = "DEBUG"

class BookmarkDataSourceImpl(db: Database) : BookmarkDataSource {
private val q = db.bookmarkTagsQueries

override suspend fun getBookmark(id: Long): Bookmark? {
return withContext(Dispatchers.IO) {
q.getBookmarkByIDwithTags(id).executeAsOneOrNull()?.toBookmark()
}
}

override fun getBookmarks(): Flow<List<Bookmark>> {
return q.getBookmarksWithTags().asFlow().mapToList(Dispatchers.IO).map { list ->
list.map { it.toBookmark() }
}
}

override suspend fun insertBookmarks(bookmarks: List<Bookmark>) {
bookmarks.forEach {
insertBookmark(it)
}
}

override suspend fun insertBookmark(bookmark: Bookmark) {
Log.d(
TAG, "insertBOOKMARK: ${bookmark.title} ${bookmark.website_title}"
)
Log.d(
TAG, "insertTAGS: ${bookmark.tags}"
)
withContext(Dispatchers.IO) {
q.transaction {
val id = q.insertBookmark(
id = bookmark.id,
url = bookmark.url,
title = bookmark.title,
description = bookmark.description,
notes = bookmark.notes,
website_title = bookmark.website_title,
website_description = bookmark.website_description,
is_archived = bookmark.is_archived,
unread = bookmark.unread,
shared = bookmark.shared,
date_added = bookmark.date_added,
date_modified = bookmark.date_modified
).executeAsOneOrNull()!!
bookmark.tags.forEach {
q.insertTag(name = it)
val tagID = q.getTagByName(it).executeAsOneOrNull()!!
Log.d(TAG, "inserted TAG: $it,$tagID")
q.insertTagForBookmark(
bookmarkID = id,
tagID = tagID,
)
}
}
}
}

override suspend fun deleteBookmark(id: Long) {
withContext(Dispatchers.IO) {
q.deleteBookmarkByID(id)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.yrovas.linklater.data.local

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import org.yrovas.linklater.data.Bookmark

class EmptyBookmarkSource : BookmarkDataSource {
override suspend fun getBookmark(id: Long): Bookmark? {
return null
}

override fun getBookmarks(): Flow<List<Bookmark>> {
return emptyList<List<Bookmark>>().asFlow()
}

override suspend fun insertBookmark(bookmark: Bookmark) {}
override suspend fun insertBookmarks(bookmarks: List<Bookmark>) {}
override suspend fun deleteBookmark(id: Long) {}
}
12 changes: 12 additions & 0 deletions app/src/main/java/org/yrovas/linklater/data/local/TagDataSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.yrovas.linklater.data.local

import kotlinx.coroutines.flow.Flow
import org.yrovas.linklater.data.Bookmark

interface TagDataSource {
suspend fun getTag(id: Long): String?
fun getTags(): Flow<List<String>>
// suspend fun insertTag(name: String)
// suspend fun insertTags(names: List<String>)
// suspend fun deleteTag(id: Long)
}
Loading

0 comments on commit 5ae7d3d

Please sign in to comment.