mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-17 16:47:01 +05:30
Merge #75
75: Use SQLDelight instead of Room r=msfjarvis a=Skrilltrax Deletes all the existing Room database code and replaces it with a simplified SQLDelight-backed implementation ready for Kotlin Multiplatform. Co-authored-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Aditya Wasan <adityawasan55@gmail.com>
This commit is contained in:
commit
23ec6ea9b1
52 changed files with 529 additions and 705 deletions
3
.idea/gradle.xml
generated
3
.idea/gradle.xml
generated
|
@ -11,9 +11,10 @@
|
|||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/api" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
<option value="$PROJECT_DIR$/model" />
|
||||
<option value="$PROJECT_DIR$/database" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
|
|
0
model/.gitignore → api/.gitignore
vendored
0
model/.gitignore → api/.gitignore
vendored
|
@ -7,10 +7,10 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
kapt(Dependencies.AndroidX.Hilt.daggerCompiler)
|
||||
kapt(Dependencies.ThirdParty.Moshi.codegen)
|
||||
api(Dependencies.ThirdParty.Retrofit.lib)
|
||||
implementation(project(":database"))
|
||||
implementation(Dependencies.AndroidX.Hilt.dagger)
|
||||
implementation(Dependencies.ThirdParty.Moshi.lib)
|
||||
implementation(Dependencies.ThirdParty.Moshi.moshiMetadataReflect)
|
||||
implementation(Dependencies.ThirdParty.Retrofit.moshi)
|
||||
testImplementation(Dependencies.Kotlin.Coroutines.core)
|
||||
testImplementation(Dependencies.Testing.junit)
|
|
@ -1,6 +1,6 @@
|
|||
package dev.msfjarvis.lobsters.data.api
|
||||
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.msfjarvis.lobsters.injection
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.zacsweers.moshix.reflect.MetadataKotlinJsonAdapterFactory
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object MoshiModule {
|
||||
@Provides
|
||||
@Reusable
|
||||
fun provideMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.add(MetadataKotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@Provides
|
||||
@Reusable
|
||||
fun provideSubmitterJsonAdapter(moshi: Moshi): JsonAdapter<Submitter> {
|
||||
return moshi.adapter()
|
||||
}
|
||||
}
|
|
@ -55,7 +55,7 @@ class LobstersApiTest {
|
|||
fun `no moderator posts in test data`() = runBlocking {
|
||||
val posts = apiClient.getHottestPosts(1)
|
||||
val moderatorPosts = posts.asSequence()
|
||||
.filter { it.submitterUser.isModerator }
|
||||
.filter { it.submitter_user.isModerator }
|
||||
.toSet()
|
||||
assertTrue(moderatorPosts.isEmpty())
|
||||
}
|
|
@ -2,7 +2,6 @@ plugins {
|
|||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
kotlin("plugin.serialization") version "1.4.21"
|
||||
id("dagger.hilt.android.plugin")
|
||||
`versioning-plugin`
|
||||
`lobsters-plugin`
|
||||
|
@ -13,9 +12,6 @@ android {
|
|||
defaultConfig {
|
||||
applicationId = "dev.msfjarvis.lobsters"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
javaCompileOptions.annotationProcessorOptions {
|
||||
argument("room.schemaLocation", "${projectDir}/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures.compose = true
|
||||
|
@ -28,9 +24,8 @@ android {
|
|||
dependencies {
|
||||
|
||||
kapt(Dependencies.AndroidX.Hilt.daggerCompiler)
|
||||
kapt(Dependencies.AndroidX.Room.compiler)
|
||||
kapt(Dependencies.ThirdParty.Roomigrant.compiler)
|
||||
implementation(project(":model"))
|
||||
implementation(project(":api"))
|
||||
implementation(project(":database"))
|
||||
implementation(Dependencies.AndroidX.appCompat)
|
||||
implementation(Dependencies.AndroidX.browser)
|
||||
implementation(Dependencies.AndroidX.Compose.compiler)
|
||||
|
@ -46,12 +41,9 @@ dependencies {
|
|||
implementation(Dependencies.AndroidX.Hilt.dagger)
|
||||
implementation(Dependencies.AndroidX.Lifecycle.runtimeKtx)
|
||||
implementation(Dependencies.AndroidX.Lifecycle.viewmodelKtx)
|
||||
implementation(Dependencies.AndroidX.Room.runtime)
|
||||
implementation(Dependencies.AndroidX.Room.ktx)
|
||||
implementation(Dependencies.Kotlin.Coroutines.android)
|
||||
implementation(Dependencies.ThirdParty.accompanist)
|
||||
implementation(Dependencies.ThirdParty.Moshi.lib)
|
||||
implementation(Dependencies.ThirdParty.Roomigrant.runtime)
|
||||
testImplementation(Dependencies.Testing.junit)
|
||||
androidTestImplementation(Dependencies.Testing.daggerHilt)
|
||||
androidTestImplementation(Dependencies.Testing.uiTest)
|
||||
|
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -1,3 +1,5 @@
|
|||
-keepattributes *Annotation*, EnclosingMethod, InnerClasses
|
||||
-dontwarn org.jetbrains.kotlin.**
|
||||
-dontwarn androidx.compose.animation.tooling.ComposeAnimation
|
||||
-dontobfuscate
|
||||
-dontoptimize
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 2,
|
||||
"identityHash": "5146546ebef999689c82a1b89e667eb4",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "lobsters_posts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shortId` TEXT NOT NULL, `shortIdUrl` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `score` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `commentCount` INTEGER NOT NULL, `description` TEXT NOT NULL, `commentsUrl` TEXT NOT NULL, `submitterUser` TEXT NOT NULL, `tags` TEXT NOT NULL, `isLiked` INTEGER NOT NULL, PRIMARY KEY(`shortId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "post.shortId",
|
||||
"columnName": "shortId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.shortIdUrl",
|
||||
"columnName": "shortIdUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.score",
|
||||
"columnName": "score",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.flags",
|
||||
"columnName": "flags",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentCount",
|
||||
"columnName": "commentCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentsUrl",
|
||||
"columnName": "commentsUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.submitterUser",
|
||||
"columnName": "submitterUser",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.isLiked",
|
||||
"columnName": "isLiked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"shortId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "lobsters_saved_posts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shortId` TEXT NOT NULL, `shortIdUrl` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `score` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `commentCount` INTEGER NOT NULL, `description` TEXT NOT NULL, `commentsUrl` TEXT NOT NULL, `submitterUser` TEXT NOT NULL, `tags` TEXT NOT NULL, `isLiked` INTEGER NOT NULL, PRIMARY KEY(`shortId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "post.shortId",
|
||||
"columnName": "shortId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.shortIdUrl",
|
||||
"columnName": "shortIdUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.score",
|
||||
"columnName": "score",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.flags",
|
||||
"columnName": "flags",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentCount",
|
||||
"columnName": "commentCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentsUrl",
|
||||
"columnName": "commentsUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.submitterUser",
|
||||
"columnName": "submitterUser",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.isLiked",
|
||||
"columnName": "isLiked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"shortId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5146546ebef999689c82a1b89e667eb4')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,186 +0,0 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 3,
|
||||
"identityHash": "fb910a30af3f2c97fcd1f530c798e6e5",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "lobsters_posts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shortId` TEXT NOT NULL, `shortIdUrl` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `score` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `commentCount` INTEGER NOT NULL, `description` TEXT NOT NULL, `commentsUrl` TEXT NOT NULL, `submitterUser` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`shortId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "post.shortId",
|
||||
"columnName": "shortId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.shortIdUrl",
|
||||
"columnName": "shortIdUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.score",
|
||||
"columnName": "score",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.flags",
|
||||
"columnName": "flags",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentCount",
|
||||
"columnName": "commentCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentsUrl",
|
||||
"columnName": "commentsUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.submitterUser",
|
||||
"columnName": "submitterUser",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"shortId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "lobsters_saved_posts",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`shortId` TEXT NOT NULL, `shortIdUrl` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `title` TEXT NOT NULL, `url` TEXT NOT NULL, `score` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `commentCount` INTEGER NOT NULL, `description` TEXT NOT NULL, `commentsUrl` TEXT NOT NULL, `submitterUser` TEXT NOT NULL, `tags` TEXT NOT NULL, PRIMARY KEY(`shortId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "post.shortId",
|
||||
"columnName": "shortId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.shortIdUrl",
|
||||
"columnName": "shortIdUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.createdAt",
|
||||
"columnName": "createdAt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.score",
|
||||
"columnName": "score",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.flags",
|
||||
"columnName": "flags",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentCount",
|
||||
"columnName": "commentCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.commentsUrl",
|
||||
"columnName": "commentsUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.submitterUser",
|
||||
"columnName": "submitterUser",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "post.tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"shortId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fb910a30af3f2c97fcd1f530c798e6e5')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.data.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
|
||||
@Entity(
|
||||
tableName = "lobsters_posts",
|
||||
primaryKeys = ["shortId"],
|
||||
)
|
||||
data class LobstersEntity(
|
||||
@Embedded
|
||||
val post: LobstersPost
|
||||
)
|
|
@ -1,14 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.data.model
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
|
||||
@Entity(
|
||||
tableName = "lobsters_saved_posts",
|
||||
primaryKeys = ["shortId"],
|
||||
)
|
||||
data class SavedLobstersEntity(
|
||||
@Embedded
|
||||
val post: LobstersPost
|
||||
)
|
|
@ -2,7 +2,7 @@ package dev.msfjarvis.lobsters.data.remote
|
|||
|
||||
import androidx.paging.PagingSource
|
||||
import dev.msfjarvis.lobsters.data.api.LobstersApi
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import javax.inject.Inject
|
||||
|
||||
class LobstersPagingSource @Inject constructor(
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package dev.msfjarvis.lobsters.data.repo
|
||||
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.database.LobstersDatabase
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class LobstersRepository @Inject constructor(private val lobstersDatabase: LobstersDatabase) {
|
||||
|
||||
private val savedPostsCache: MutableMap<String, LobstersPost> = mutableMapOf()
|
||||
private val coroutineScope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
getAllPosts().forEach {
|
||||
savedPostsCache.putIfAbsent(it.short_id, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isPostSaved(postId: String): Boolean {
|
||||
return savedPostsCache.containsKey(postId)
|
||||
}
|
||||
|
||||
fun getPostFromCache(postId: String): LobstersPost? {
|
||||
return savedPostsCache[postId]
|
||||
}
|
||||
|
||||
fun getAllPostsFromCache(): List<LobstersPost> {
|
||||
return savedPostsCache.values.toList()
|
||||
}
|
||||
|
||||
private suspend fun getPost(postId: String): LobstersPost? = withContext(Dispatchers.IO) {
|
||||
return@withContext lobstersDatabase.postQueries.selectPost(postId).executeAsOneOrNull()
|
||||
}
|
||||
|
||||
private suspend fun getAllPosts(): List<LobstersPost> = withContext(Dispatchers.IO) {
|
||||
return@withContext lobstersDatabase.postQueries.selectAllPosts().executeAsList()
|
||||
}
|
||||
|
||||
suspend fun addPost(post: LobstersPost) = withContext(Dispatchers.IO) {
|
||||
if (!savedPostsCache.containsKey(post.short_id)) {
|
||||
savedPostsCache.putIfAbsent(post.short_id, post)
|
||||
lobstersDatabase.postQueries.insertOrReplacePost(post)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePost(post: LobstersPost) = withContext(Dispatchers.IO) {
|
||||
if (savedPostsCache.containsKey(post.short_id)) {
|
||||
savedPostsCache.remove(post.short_id)
|
||||
lobstersDatabase.postQueries.deletePost(post.short_id)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.data.source
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.squareup.moshi.Moshi
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.msfjarvis.lobsters.model.SubmitterJsonAdapter
|
||||
|
||||
object LobstersApiTypeConverters {
|
||||
private const val SEPARATOR = ","
|
||||
private val moshi = Moshi.Builder().build()
|
||||
private val submitterAdapter = SubmitterJsonAdapter(moshi)
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun toSubmitterUser(value: String?): Submitter? {
|
||||
return value?.let { submitterAdapter.fromJson(value) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromSubmitterUser(value: Submitter?): String? {
|
||||
return submitterAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun toTagList(value: String?): List<String>? {
|
||||
return value?.split(SEPARATOR)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun fromTagList(value: List<String>?): String? {
|
||||
return value?.joinToString(SEPARATOR)
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.data.source
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import dev.msfjarvis.lobsters.data.model.LobstersEntity
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class PostsDao {
|
||||
@Query("SELECT * FROM lobsters_posts")
|
||||
abstract fun loadPosts(): Flow<List<LobstersPost>>
|
||||
|
||||
@Update
|
||||
suspend fun updatePost(vararg posts: LobstersPost) {
|
||||
updatePosts(posts.map { LobstersEntity(it) })
|
||||
}
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||
protected abstract suspend fun updatePosts(posts: List<LobstersEntity>)
|
||||
|
||||
|
||||
@Transaction
|
||||
open suspend fun insertPosts(vararg posts: LobstersPost) {
|
||||
insertPosts(posts.map { LobstersEntity(it) })
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
protected abstract suspend fun insertPosts(posts: List<LobstersEntity>)
|
||||
|
||||
@Transaction
|
||||
open suspend fun deletePosts(vararg posts: LobstersPost) {
|
||||
deletePosts(posts.map { LobstersEntity(it) })
|
||||
}
|
||||
|
||||
@Delete
|
||||
protected abstract suspend fun deletePosts(posts: List<LobstersEntity>)
|
||||
|
||||
@Query("DELETE FROM lobsters_posts")
|
||||
abstract suspend fun deleteAllPosts()
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.data.source
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import dev.matrix.roomigrant.GenerateRoomMigrations
|
||||
import dev.msfjarvis.lobsters.data.model.LobstersEntity
|
||||
import dev.msfjarvis.lobsters.data.model.SavedLobstersEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
LobstersEntity::class,
|
||||
SavedLobstersEntity::class
|
||||
],
|
||||
version = 3,
|
||||
exportSchema = true,
|
||||
)
|
||||
@TypeConverters(
|
||||
LobstersApiTypeConverters::class,
|
||||
)
|
||||
@GenerateRoomMigrations
|
||||
abstract class PostsDatabase : RoomDatabase() {
|
||||
abstract fun postsDao(): PostsDao
|
||||
abstract fun savedPostsDao(): SavedPostsDao
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.data.source
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import dev.msfjarvis.lobsters.data.model.SavedLobstersEntity
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class SavedPostsDao {
|
||||
@Query("SELECT * FROM lobsters_saved_posts")
|
||||
abstract fun loadPosts(): Flow<List<LobstersPost>>
|
||||
|
||||
@Transaction
|
||||
open suspend fun insertPosts(vararg posts: LobstersPost) {
|
||||
insertPosts(posts.map { SavedLobstersEntity(it) })
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
protected abstract suspend fun insertPosts(posts: List<SavedLobstersEntity>)
|
||||
|
||||
@Transaction
|
||||
open suspend fun deletePosts(vararg posts: LobstersPost) {
|
||||
deletePosts(posts.map { SavedLobstersEntity(it) })
|
||||
}
|
||||
|
||||
@Delete
|
||||
protected abstract suspend fun deletePosts(posts: List<SavedLobstersEntity>)
|
||||
|
||||
@Query("DELETE FROM lobsters_saved_posts")
|
||||
abstract suspend fun deleteAllPosts()
|
||||
|
||||
@Query("DELETE FROM lobsters_saved_posts WHERE shortId LIKE :shortId")
|
||||
abstract suspend fun deletePostById(shortId: String)
|
||||
|
||||
@Query("SELECT EXISTS(SELECT 1 FROM lobsters_saved_posts WHERE shortId LIKE :shortId)")
|
||||
abstract suspend fun isLiked(shortId: String): Boolean
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.injection
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dev.msfjarvis.lobsters.data.source.PostsDatabase
|
||||
import dev.msfjarvis.lobsters.data.source.PostsDatabase_Migrations
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
object PersistenceModule {
|
||||
|
||||
@Provides
|
||||
fun providePostsDatabase(@ApplicationContext context: Context): PostsDatabase {
|
||||
return Room.databaseBuilder(context, PostsDatabase::class.java, "posts.db")
|
||||
.addMigrations(*PostsDatabase_Migrations.build())
|
||||
.build()
|
||||
}
|
||||
}
|
|
@ -78,14 +78,15 @@ fun LobstersApp() {
|
|||
HottestPosts(
|
||||
posts = hottestPosts,
|
||||
listState = hottestPostsListState,
|
||||
saveAction = viewModel::savePost,
|
||||
isPostSaved = viewModel::isPostSaved,
|
||||
saveAction = viewModel::toggleSave,
|
||||
modifier = Modifier.padding(bottom = innerPadding.bottom),
|
||||
)
|
||||
}
|
||||
composable(Destination.Saved.route) {
|
||||
SavedPosts(
|
||||
posts = savedPosts,
|
||||
saveAction = viewModel::removeSavedPost,
|
||||
saveAction = viewModel::toggleSave,
|
||||
modifier = Modifier.padding(bottom = innerPadding.bottom),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,17 +3,22 @@ package dev.msfjarvis.lobsters.ui.posts
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.ui.urllauncher.AmbientUrlLauncher
|
||||
|
||||
@Composable
|
||||
fun HottestPosts(
|
||||
posts: LazyPagingItems<LobstersPost>,
|
||||
listState: LazyListState,
|
||||
isPostSaved: (String) -> Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
saveAction: (LobstersPost) -> Unit,
|
||||
) {
|
||||
|
@ -28,11 +33,17 @@ fun HottestPosts(
|
|||
) {
|
||||
items(posts) { item ->
|
||||
if (item != null) {
|
||||
var isSaved by remember(item.short_id) { mutableStateOf(isPostSaved(item.short_id)) }
|
||||
|
||||
LobstersItem(
|
||||
post = item,
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.commentsUrl }) },
|
||||
onLongClick = { urlLauncher.launch(item.commentsUrl) },
|
||||
onSaveButtonClick = { saveAction.invoke(item) },
|
||||
isSaved = isSaved,
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.comments_url }) },
|
||||
onLongClick = { urlLauncher.launch(item.comments_url) },
|
||||
onSaveButtonClick = {
|
||||
isSaved = isSaved.not()
|
||||
saveAction.invoke(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.msfjarvis.lobsters.ui.posts
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
@ -10,9 +11,9 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.IconToggleButton
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -24,7 +25,7 @@ import coil.transform.CircleCropTransformation
|
|||
import dev.chrisbanes.accompanist.coil.CoilImage
|
||||
import dev.msfjarvis.lobsters.R
|
||||
import dev.msfjarvis.lobsters.data.api.LobstersApi
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
|
||||
import dev.msfjarvis.lobsters.ui.theme.titleColor
|
||||
|
@ -60,6 +61,7 @@ val TEST_POST = LobstersPost(
|
|||
@Composable
|
||||
fun LobstersItem(
|
||||
post: LobstersPost,
|
||||
isSaved: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onSaveButtonClick: () -> Unit,
|
||||
|
@ -96,7 +98,7 @@ fun LobstersItem(
|
|||
.padding(vertical = 8.dp),
|
||||
)
|
||||
CoilImage(
|
||||
data = "${LobstersApi.BASE_URL}/${post.submitterUser.avatarUrl}",
|
||||
data = "${LobstersApi.BASE_URL}/${post.submitter_user.avatarUrl}",
|
||||
fadeIn = true,
|
||||
requestBuilder = {
|
||||
transformations(CircleCropTransformation())
|
||||
|
@ -110,7 +112,7 @@ fun LobstersItem(
|
|||
},
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.submitted_by, post.submitterUser.username),
|
||||
text = stringResource(id = R.string.submitted_by, post.submitter_user.username),
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.constrainAs(submitter) {
|
||||
|
@ -119,20 +121,23 @@ fun LobstersItem(
|
|||
start.linkTo(avatar.end)
|
||||
},
|
||||
)
|
||||
IconResource(
|
||||
resourceId = R.drawable.ic_favorite_border_24px,
|
||||
IconToggleButton(
|
||||
checked = isSaved,
|
||||
onCheckedChange = { onSaveButtonClick.invoke() },
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.clickable(
|
||||
onClick = onSaveButtonClick,
|
||||
indication = rememberRipple(),
|
||||
)
|
||||
.constrainAs(saveButton) {
|
||||
end.linkTo(parent.end)
|
||||
centerVerticallyTo(parent)
|
||||
},
|
||||
tint = Color(0xFFD97373),
|
||||
)
|
||||
}
|
||||
) {
|
||||
Crossfade(current = isSaved) {
|
||||
if (it) {
|
||||
IconResource(resourceId = R.drawable.ic_favorite_24px, tint = Color(0xFFD97373))
|
||||
} else {
|
||||
IconResource(resourceId = R.drawable.ic_favorite_border_24px, tint = Color(0xFFD97373))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,6 +171,7 @@ fun Preview() {
|
|||
items(listOf(TEST_POST, TEST_POST, TEST_POST, TEST_POST, TEST_POST)) { item ->
|
||||
LobstersItem(
|
||||
post = item,
|
||||
isSaved = false,
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
onSaveButtonClick = {},
|
||||
|
|
|
@ -4,7 +4,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.ui.urllauncher.AmbientUrlLauncher
|
||||
|
||||
@Composable
|
||||
|
@ -26,8 +26,9 @@ fun SavedPosts(
|
|||
items(posts) { item ->
|
||||
LobstersItem(
|
||||
post = item,
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.commentsUrl }) },
|
||||
onLongClick = { urlLauncher.launch(item.commentsUrl) },
|
||||
isSaved = true,
|
||||
onClick = { urlLauncher.launch(item.url.ifEmpty { item.comments_url }) },
|
||||
onLongClick = { urlLauncher.launch(item.comments_url) },
|
||||
onSaveButtonClick = { saveAction.invoke(item) },
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,49 +4,49 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.remote.LobstersPagingSource
|
||||
import dev.msfjarvis.lobsters.data.source.PostsDatabase
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LobstersViewModel @Inject constructor(
|
||||
private val lobstersRepository: LobstersRepository,
|
||||
private val pagingSource: LobstersPagingSource,
|
||||
database: PostsDatabase,
|
||||
) : ViewModel() {
|
||||
private val _savedPosts = MutableStateFlow<List<LobstersPost>>(emptyList())
|
||||
private val savedPostsDao = database.savedPostsDao()
|
||||
val savedPosts: StateFlow<List<LobstersPost>> get() = _savedPosts
|
||||
private val _savedPosts = MutableStateFlow(lobstersRepository.getAllPostsFromCache())
|
||||
val savedPosts = _savedPosts.asStateFlow()
|
||||
val posts = Pager(PagingConfig(25)) {
|
||||
pagingSource
|
||||
}.flow
|
||||
}.flow.cachedIn(viewModelScope)
|
||||
|
||||
init {
|
||||
getSavedPosts()
|
||||
}
|
||||
|
||||
private fun getSavedPosts() {
|
||||
fun toggleSave(post: LobstersPost) {
|
||||
viewModelScope.launch {
|
||||
savedPostsDao.loadPosts().collectLatest { _savedPosts.value = it }
|
||||
val isSaved = lobstersRepository.isPostSaved(post.short_id)
|
||||
if (isSaved) removeSavedPost(post) else savePost(post)
|
||||
}
|
||||
}
|
||||
|
||||
fun savePost(post: LobstersPost) {
|
||||
fun isPostSaved(postId: String): Boolean {
|
||||
return lobstersRepository.isPostSaved(postId)
|
||||
}
|
||||
|
||||
private fun savePost(post: LobstersPost) {
|
||||
viewModelScope.launch {
|
||||
savedPostsDao.insertPosts(post)
|
||||
getSavedPosts()
|
||||
lobstersRepository.addPost(post)
|
||||
_savedPosts.value = lobstersRepository.getAllPostsFromCache()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSavedPost(post: LobstersPost) {
|
||||
private fun removeSavedPost(post: LobstersPost) {
|
||||
viewModelScope.launch {
|
||||
savedPostsDao.deletePostById(post.shortId)
|
||||
getSavedPosts()
|
||||
lobstersRepository.removePost(post)
|
||||
_savedPosts.value = lobstersRepository.getAllPostsFromCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ buildscript {
|
|||
classpath(build.getValue("androidGradlePlugin"))
|
||||
classpath(build.getValue("daggerGradlePlugin"))
|
||||
classpath(build.getValue("kotlinGradlePlugin"))
|
||||
classpath(build.getValue("sqldelightGradlePlugin"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,4 +39,5 @@ dependencies {
|
|||
implementation(build.getValue("androidGradlePlugin_builderModel"))
|
||||
implementation(build.getValue("androidGradlePlugin_lintModel"))
|
||||
implementation(build.getValue("jsemver"))
|
||||
implementation(build.getValue("sqldelightGradlePlugin"))
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ rootProject.ext.versions = [
|
|||
kotlin : '1.4.21',
|
||||
lint : '30.0.0-alpha05',
|
||||
semver : '0.9.0',
|
||||
sqldelight: '1.4.4',
|
||||
]
|
||||
|
||||
rootProject.ext.build = [
|
||||
|
@ -14,4 +15,5 @@ rootProject.ext.build = [
|
|||
daggerGradlePlugin : "com.google.dagger:hilt-android-gradle-plugin:${versions.daggerHilt}",
|
||||
kotlinGradlePlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}",
|
||||
jsemver : "com.github.zafarkhaja:java-semver:${versions.semver}",
|
||||
sqldelightGradlePlugin : "com.squareup.sqldelight:gradle-plugin:${versions.sqldelight}",
|
||||
]
|
||||
|
|
|
@ -65,7 +65,6 @@ internal fun BaseAppModuleExtension.configureAndroidApplicationOptions(project:
|
|||
project.tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=androidx.compose.material.ExperimentalMaterialApi"
|
||||
)
|
||||
|
|
|
@ -49,14 +49,6 @@ object Dependencies {
|
|||
const val runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
|
||||
const val viewmodelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version"
|
||||
}
|
||||
|
||||
object Room {
|
||||
|
||||
private const val version = "2.3.0-beta01"
|
||||
const val compiler = "androidx.room:room-compiler:$version"
|
||||
const val ktx = "androidx.room:room-ktx:$version"
|
||||
const val runtime = "androidx.room:room-runtime:$version"
|
||||
}
|
||||
}
|
||||
|
||||
object ThirdParty {
|
||||
|
@ -66,8 +58,8 @@ object Dependencies {
|
|||
object Moshi {
|
||||
|
||||
private const val version = "1.11.0"
|
||||
const val codegen = "com.squareup.moshi:moshi-kotlin-codegen:$version"
|
||||
const val lib = "com.squareup.moshi:moshi:$version"
|
||||
const val moshiMetadataReflect = "dev.zacsweers.moshix:moshi-metadata-reflect:0.7.1"
|
||||
}
|
||||
|
||||
object Retrofit {
|
||||
|
@ -77,11 +69,11 @@ object Dependencies {
|
|||
const val moshi = "com.squareup.retrofit2:converter-moshi:$version"
|
||||
}
|
||||
|
||||
object Roomigrant {
|
||||
object SQLDelight {
|
||||
|
||||
private const val version = "0.2.0"
|
||||
const val compiler = "com.github.MatrixDev.Roomigrant:RoomigrantCompiler:$version"
|
||||
const val runtime = "com.github.MatrixDev.Roomigrant:RoomigrantLib:$version"
|
||||
private const val version = "1.4.4"
|
||||
const val jvmDriver = "com.squareup.sqldelight:sqlite-driver:$version"
|
||||
const val androidDriver = "com.squareup.sqldelight:android-driver:$version"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
|
||||
internal val additionalCompilerArgs = listOf(
|
||||
"-Xallow-jvm-ir-dependencies",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xskip-prerelease-check"
|
||||
)
|
||||
|
|
|
@ -7,6 +7,8 @@ import com.android.build.gradle.TestedExtension
|
|||
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
|
||||
import com.android.build.gradle.internal.plugins.AppPlugin
|
||||
import com.android.build.gradle.internal.plugins.LibraryPlugin
|
||||
import com.squareup.sqldelight.gradle.SqlDelightExtension
|
||||
import com.squareup.sqldelight.gradle.SqlDelightPlugin
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.plugins.JavaLibraryPlugin
|
||||
|
@ -46,6 +48,9 @@ class LobstersPlugin : Plugin<Project> {
|
|||
is Kapt3GradleSubplugin -> {
|
||||
project.configureKapt()
|
||||
}
|
||||
is SqlDelightPlugin -> {
|
||||
project.extensions.getByType<SqlDelightExtension>().configureLobstersDatabase()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
8
buildSrc/src/main/java/SqlDelightConfig.kt
Normal file
8
buildSrc/src/main/java/SqlDelightConfig.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
import com.squareup.sqldelight.gradle.SqlDelightExtension
|
||||
|
||||
internal fun SqlDelightExtension.configureLobstersDatabase() {
|
||||
database("LobstersDatabase") {
|
||||
packageName = "dev.msfjarvis.lobsters.database"
|
||||
sourceFolders = listOf("sqldelight")
|
||||
}
|
||||
}
|
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
18
database/build.gradle.kts
Normal file
18
database/build.gradle.kts
Normal file
|
@ -0,0 +1,18 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
id("com.squareup.sqldelight")
|
||||
`lobsters-plugin`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
kapt(Dependencies.AndroidX.Hilt.daggerCompiler)
|
||||
implementation(Dependencies.AndroidX.Hilt.dagger)
|
||||
implementation(Dependencies.ThirdParty.Moshi.lib)
|
||||
implementation(Dependencies.ThirdParty.Moshi.moshiMetadataReflect)
|
||||
implementation(Dependencies.ThirdParty.SQLDelight.androidDriver)
|
||||
testImplementation(Dependencies.Kotlin.Coroutines.core)
|
||||
testImplementation(Dependencies.ThirdParty.SQLDelight.jvmDriver)
|
||||
testImplementation(Dependencies.Testing.junit)
|
||||
}
|
0
database/consumer-rules.pro
Normal file
0
database/consumer-rules.pro
Normal file
21
database/proguard-rules.pro
vendored
Normal file
21
database/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
4
database/src/main/AndroidManifest.xml
Normal file
4
database/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="dev.msfjarvis.lobsters.database">
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,46 @@
|
|||
package dev.msfjarvis.lobsters.injection
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dev.msfjarvis.lobsters.data.local.LobstersPost
|
||||
import dev.msfjarvis.lobsters.database.LobstersDatabase
|
||||
import dev.msfjarvis.lobsters.model.SubmitterAdapter
|
||||
import dev.msfjarvis.lobsters.model.TagsAdapter
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Reusable
|
||||
fun providesTagsAdapter(): TagsAdapter {
|
||||
return TagsAdapter()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesSqlDriver(@ApplicationContext context: Context): SqlDriver {
|
||||
return AndroidSqliteDriver(LobstersDatabase.Schema, context, "SavedPosts.db")
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesLobstersDatabase(
|
||||
sqlDriver: SqlDriver,
|
||||
submitterAdapter: SubmitterAdapter,
|
||||
tagsAdapter: TagsAdapter
|
||||
): LobstersDatabase {
|
||||
return LobstersDatabase(
|
||||
sqlDriver,
|
||||
LobstersPost.Adapter(submitterAdapter, tagsAdapter)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class KeybaseSignature(
|
||||
@Json(name = "kb_username")
|
||||
val kbUsername: String,
|
|
@ -1,9 +1,7 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class Submitter(
|
||||
val username: String,
|
||||
@Json(name = "created_at")
|
|
@ -0,0 +1,17 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.sqldelight.ColumnAdapter
|
||||
import javax.inject.Inject
|
||||
|
||||
class SubmitterAdapter @Inject constructor(private val submitterJsonAdapter: JsonAdapter<Submitter>) :
|
||||
ColumnAdapter<Submitter, String> {
|
||||
|
||||
override fun decode(databaseValue: String): Submitter {
|
||||
return submitterJsonAdapter.fromJson(databaseValue)!!
|
||||
}
|
||||
|
||||
override fun encode(value: Submitter): String {
|
||||
return submitterJsonAdapter.toJson(value)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.sqldelight.ColumnAdapter
|
||||
|
||||
class TagsAdapter : ColumnAdapter<List<String>, String> {
|
||||
override fun decode(databaseValue: String): List<String> {
|
||||
return databaseValue.split(SEPARATOR)
|
||||
}
|
||||
|
||||
override fun encode(value: List<String>): String {
|
||||
return value.joinToString(SEPARATOR)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val SEPARATOR = ","
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import dev.msfjarvis.lobsters.model.Submitter;
|
||||
import java.lang.Boolean;
|
||||
import kotlin.collections.List;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS LobstersPost(
|
||||
short_id TEXT NOT NULL PRIMARY KEY,
|
||||
short_id_url TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
score INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL,
|
||||
comment_count INTEGER NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
comments_url TEXT NOT NULL,
|
||||
submitter_user TEXT as Submitter NOT NULL,
|
||||
tags TEXT as List<String> NOT NULL
|
||||
);
|
||||
|
||||
selectPost:
|
||||
SELECT *
|
||||
FROM LobstersPost
|
||||
WHERE short_id = ?;
|
||||
|
||||
selectAllPosts:
|
||||
SELECT *
|
||||
FROM LobstersPost;
|
||||
|
||||
insertOrReplacePost:
|
||||
INSERT OR REPLACE
|
||||
INTO LobstersPost
|
||||
VALUES ?;
|
||||
|
||||
deletePost:
|
||||
DELETE
|
||||
FROM LobstersPost
|
||||
WHERE short_id = ?;
|
||||
|
||||
deleteAllPosts:
|
||||
DELETE
|
||||
FROM LobstersPost;
|
||||
|
||||
selectCount:
|
||||
SELECT COUNT(*)
|
||||
FROM LobstersPost;
|
|
@ -0,0 +1,173 @@
|
|||
package dev.msfjarvis.lobsters.data.local
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||
import dev.msfjarvis.lobsters.database.LobstersDatabase
|
||||
import dev.msfjarvis.lobsters.model.Submitter
|
||||
import dev.msfjarvis.lobsters.model.SubmitterAdapter
|
||||
import dev.msfjarvis.lobsters.model.TagsAdapter
|
||||
import dev.zacsweers.moshix.reflect.MetadataKotlinJsonAdapterFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class SqlDelightQueriesTest {
|
||||
|
||||
private lateinit var postQueries: PostQueries
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val moshi = Moshi.Builder().add(MetadataKotlinJsonAdapterFactory()).build()
|
||||
val submitterJsonAdapter = moshi.adapter<Submitter>()
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
LobstersDatabase.Schema.create(driver)
|
||||
val database = LobstersDatabase(
|
||||
driver,
|
||||
LobstersPost.Adapter(SubmitterAdapter(submitterJsonAdapter), TagsAdapter())
|
||||
)
|
||||
postQueries = database.postQueries
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectCount() = runBlocking {
|
||||
val posts = createTestData(5)
|
||||
|
||||
posts.forEach { postQueries.insertOrReplacePost(it) }
|
||||
|
||||
val postCount = postQueries.selectCount().executeAsOne()
|
||||
assertEquals(5, postCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertIntoDatabase() = runBlocking {
|
||||
// Get 5 posts
|
||||
val posts = createTestData(5)
|
||||
|
||||
// Insert posts into DB
|
||||
posts.forEach { postQueries.insertOrReplacePost(it) }
|
||||
|
||||
// Check post count
|
||||
val postsCount = postQueries.selectCount().executeAsOne()
|
||||
assertEquals(5, postsCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceFromDatabase() = runBlocking {
|
||||
// Get 1 post
|
||||
val post = createTestData(1)[0]
|
||||
|
||||
// Insert post into DB
|
||||
postQueries.insertOrReplacePost(post)
|
||||
|
||||
// Create a new post and try replacing it
|
||||
val newPost = post.copy(comment_count = 100)
|
||||
postQueries.insertOrReplacePost(newPost)
|
||||
|
||||
// Check post count
|
||||
val postsCount = postQueries.selectCount().executeAsOne()
|
||||
assertEquals(1, postsCount)
|
||||
|
||||
// Check if post is updated
|
||||
val postFromDb = postQueries.selectPost(post.short_id).executeAsOne()
|
||||
assertEquals(100, postFromDb.comment_count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectPost() = runBlocking {
|
||||
// Get 1 post
|
||||
val post = createTestData(1)[0]
|
||||
|
||||
// Insert post into DB
|
||||
postQueries.insertOrReplacePost(post)
|
||||
|
||||
val postFromDb = postQueries.selectAllPosts().executeAsOne()
|
||||
assertEquals("test_id_1", postFromDb.short_id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectAllPosts() = runBlocking {
|
||||
// Get 5 post
|
||||
val posts = createTestData(5)
|
||||
|
||||
// Insert posts into DB
|
||||
posts.forEach { postQueries.insertOrReplacePost(it) }
|
||||
|
||||
val postsFromDb = postQueries.selectAllPosts().executeAsList()
|
||||
|
||||
// Check if all posts have correct short_id
|
||||
for (i in 1..5) {
|
||||
assertEquals("test_id_$i", postsFromDb[i - 1].short_id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deletePost() = runBlocking {
|
||||
// Create 3 posts and insert them to DB
|
||||
val posts = createTestData(3)
|
||||
posts.forEach { postQueries.insertOrReplacePost(it) }
|
||||
|
||||
// Delete 2nd post
|
||||
postQueries.deletePost("test_id_2")
|
||||
|
||||
val postsFromDB = postQueries.selectAllPosts().executeAsList()
|
||||
|
||||
// Check if size is 2, and only the correct post is deleted
|
||||
assertEquals(2, postsFromDB.size)
|
||||
assertEquals("test_id_1", postsFromDB[0].short_id)
|
||||
assertEquals("test_id_3", postsFromDB[1].short_id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllPost() = runBlocking {
|
||||
// Create 5 posts and insert them to DB
|
||||
val posts = createTestData(5)
|
||||
posts.forEach { postQueries.insertOrReplacePost(it) }
|
||||
|
||||
// Delete all posts
|
||||
postQueries.deleteAllPosts()
|
||||
|
||||
val postsCount = postQueries.selectCount().executeAsOne()
|
||||
|
||||
// Check if db is empty
|
||||
assertEquals(0, postsCount)
|
||||
}
|
||||
|
||||
|
||||
private fun createTestData(count: Int): ArrayList<LobstersPost> {
|
||||
val posts = arrayListOf<LobstersPost>()
|
||||
|
||||
for (i in 1..count) {
|
||||
val submitter = Submitter(
|
||||
username = "test_user_$i",
|
||||
createdAt = "0",
|
||||
about = "test",
|
||||
avatarUrl = "test_avatar_url",
|
||||
invitedByUser = "test_user",
|
||||
isAdmin = false,
|
||||
isModerator = false
|
||||
)
|
||||
|
||||
val post = LobstersPost(
|
||||
short_id = "test_id_$i",
|
||||
short_id_url = "test_id_url",
|
||||
created_at = "0",
|
||||
title = "test",
|
||||
url = "test_url",
|
||||
score = 0,
|
||||
flags = 0,
|
||||
comment_count = 0,
|
||||
description = "test",
|
||||
comments_url = "test_comments_url",
|
||||
submitter_user = submitter,
|
||||
tags = listOf(),
|
||||
)
|
||||
|
||||
posts.add(post)
|
||||
}
|
||||
|
||||
return posts
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.injection
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object MoshiModule {
|
||||
@Provides
|
||||
fun provideMoshi(): Moshi {
|
||||
return Moshi.Builder().build()
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package dev.msfjarvis.lobsters.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class LobstersPost(
|
||||
@Json(name = "short_id")
|
||||
val shortId: String,
|
||||
@Json(name = "short_id_url")
|
||||
val shortIdUrl: String,
|
||||
@Json(name = "created_at")
|
||||
val createdAt: String,
|
||||
val title: String,
|
||||
val url: String,
|
||||
val score: Long,
|
||||
val flags: Long,
|
||||
@Json(name = "comment_count")
|
||||
val commentCount: Long,
|
||||
val description: String,
|
||||
@Json(name = "comments_url")
|
||||
val commentsUrl: String,
|
||||
@Json(name = "submitter_user")
|
||||
val submitterUser: Submitter,
|
||||
val tags: List<String>,
|
||||
)
|
|
@ -1,3 +1,3 @@
|
|||
rootProject.name = "Claw for lobste.rs"
|
||||
include(":app", ":model")
|
||||
include(":app", ":api", ":database")
|
||||
enableFeaturePreview("GRADLE_METADATA")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue