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:
bors[bot] 2021-01-31 15:16:14 +00:00 committed by GitHub
commit 23ec6ea9b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 529 additions and 705 deletions

3
.idea/gradle.xml generated
View file

@ -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" />

View file

View file

@ -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)

View file

@ -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

View file

@ -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()
}
}

View file

@ -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())
}

View file

@ -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)

View file

@ -1,3 +1,5 @@
-keepattributes *Annotation*, EnclosingMethod, InnerClasses
-dontwarn org.jetbrains.kotlin.**
-dontwarn androidx.compose.animation.tooling.ComposeAnimation
-dontobfuscate
-dontoptimize

View file

@ -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')"
]
}
}

View file

@ -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')"
]
}
}

View file

@ -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
)

View file

@ -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
)

View file

@ -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(

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -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),
)
}

View file

@ -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)
},
)
}
}

View file

@ -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 = {},

View file

@ -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) },
)
}

View file

@ -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()
}
}
}

View file

@ -9,6 +9,7 @@ buildscript {
classpath(build.getValue("androidGradlePlugin"))
classpath(build.getValue("daggerGradlePlugin"))
classpath(build.getValue("kotlinGradlePlugin"))
classpath(build.getValue("sqldelightGradlePlugin"))
}
}

View file

@ -39,4 +39,5 @@ dependencies {
implementation(build.getValue("androidGradlePlugin_builderModel"))
implementation(build.getValue("androidGradlePlugin_lintModel"))
implementation(build.getValue("jsemver"))
implementation(build.getValue("sqldelightGradlePlugin"))
}

View file

@ -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}",
]

View file

@ -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"
)

View file

@ -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"
}
}

View file

@ -5,5 +5,6 @@
internal val additionalCompilerArgs = listOf(
"-Xallow-jvm-ir-dependencies",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xskip-prerelease-check"
)

View file

@ -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()
}
}
}
}

View 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
View file

@ -0,0 +1 @@
/build

18
database/build.gradle.kts Normal file
View 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)
}

View file

21
database/proguard-rules.pro vendored Normal file
View 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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="dev.msfjarvis.lobsters.database">
</manifest>

View file

@ -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)
)
}
}

View file

@ -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,

View file

@ -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")

View file

@ -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)
}
}

View file

@ -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 = ","
}
}

View file

@ -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;

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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>,
)

View file

@ -1,3 +1,3 @@
rootProject.name = "Claw for lobste.rs"
include(":app", ":model")
include(":app", ":api", ":database")
enableFeaturePreview("GRADLE_METADATA")