all: refactor model classes and serialization

- Move model classes to a standalone `model` Gradle module
- Migrate from Moshi to kotlinx.serialization for multiplatform support
This commit is contained in:
Harsh Shandilya 2021-09-30 12:57:23 +05:30
parent 76c46b4229
commit 096d2882e2
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
24 changed files with 155 additions and 137 deletions

View file

@ -23,7 +23,8 @@ dependencies {
implementation(libs.androidx.paging.compose) implementation(libs.androidx.paging.compose)
implementation(libs.dagger.hilt.android) implementation(libs.dagger.hilt.android)
implementation(libs.sqldelight.extensions.coroutines) implementation(libs.sqldelight.extensions.coroutines)
implementation(libs.retrofit.moshiConverter) implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit.kotlinxSerializationConverter) { isTransitive = false }
} }
android { android {

View file

@ -12,3 +12,21 @@
-keep,allowobfuscation,allowshrinking class dev.msfjarvis.claw.android.** { *; } -keep,allowobfuscation,allowshrinking class dev.msfjarvis.claw.android.** { *; }
-dontobfuscate -dontobfuscate
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class dev.msfjarvis.claw.model.**$$serializer { *; }
-keepclassmembers class dev.msfjarvis.claw.model.** {
*** Companion;
}
-keepclasseswithmembers class dev.msfjarvis.claw.model.** {
kotlinx.serialization.KSerializer serializer(...);
}

View file

@ -1,7 +1,7 @@
package dev.msfjarvis.claw.android.ext package dev.msfjarvis.claw.android.ext
import dev.msfjarvis.claw.api.model.LobstersPost
import dev.msfjarvis.claw.database.local.SavedPost import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LobstersPost
/** Convert a [LobstersPost] object returned by the API into a [SavedPost] for persistence. */ /** Convert a [LobstersPost] object returned by the API into a [SavedPost] for persistence. */
fun LobstersPost.toDbModel(): SavedPost { fun LobstersPost.toDbModel(): SavedPost {

View file

@ -1,18 +1,21 @@
package dev.msfjarvis.claw.android.injection package dev.msfjarvis.claw.android.injection
import android.util.Log import android.util.Log
import com.squareup.moshi.Moshi import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Lazy import dagger.Lazy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.api.LobstersApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create import retrofit2.create
@OptIn(ExperimentalSerializationApi::class)
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object ApiModule { object ApiModule {
@ -35,12 +38,12 @@ object ApiModule {
@Provides @Provides
fun provideRetrofit( fun provideRetrofit(
client: Lazy<OkHttpClient>, client: Lazy<OkHttpClient>,
moshi: Lazy<Moshi>,
): Retrofit { ): Retrofit {
val contentType = MediaType.get("application/json")
return Retrofit.Builder() return Retrofit.Builder()
.client(client.get()) .client(client.get())
.baseUrl(LobstersApi.BASE_URL) .baseUrl(LobstersApi.BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi.get())) .addConverterFactory(Json.asConverterFactory(contentType))
.build() .build()
} }

View file

@ -1,18 +0,0 @@
package dev.msfjarvis.claw.android.injection
import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
import dagger.Reusable
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
object MoshiModule {
@Provides
@Reusable
fun provideMoshi(): Moshi {
return Moshi.Builder().build()
}
}

View file

@ -2,7 +2,7 @@ package dev.msfjarvis.claw.android.paging
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.paging.PagingState import androidx.paging.PagingState
import dev.msfjarvis.claw.api.model.LobstersPost import dev.msfjarvis.claw.model.LobstersPost
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View file

@ -14,9 +14,9 @@ import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.items import androidx.paging.compose.items
import dev.msfjarvis.claw.android.ext.toDbModel import dev.msfjarvis.claw.android.ext.toDbModel
import dev.msfjarvis.claw.api.model.LobstersPost
import dev.msfjarvis.claw.common.posts.LobstersCard import dev.msfjarvis.claw.common.posts.LobstersCard
import dev.msfjarvis.claw.database.local.SavedPost import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LobstersPost
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable

View file

@ -1,14 +1,12 @@
plugins { plugins { kotlin("jvm") }
kotlin("jvm")
id("com.google.devtools.ksp") version "1.5.31-1.0.0"
}
dependencies { dependencies {
api(projects.model)
api(libs.retrofit.lib) api(libs.retrofit.lib)
ksp(libs.moshix.ksp) implementation(libs.kotlinx.serialization.core)
implementation(libs.moshi.lib) testImplementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit.moshiConverter) { exclude(group = "com.squareup.moshi") }
testImplementation(libs.kotlin.coroutines.core) testImplementation(libs.kotlin.coroutines.core)
testImplementation(kotlin("test-junit")) testImplementation(kotlin("test-junit"))
testImplementation(libs.retrofit.kotlinxSerializationConverter) { isTransitive = false }
testImplementation(libs.testing.mockWebServer) testImplementation(libs.testing.mockWebServer)
} }

View file

@ -1,7 +1,7 @@
package dev.msfjarvis.claw.api package dev.msfjarvis.claw.api
import dev.msfjarvis.claw.api.model.LobstersPost import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.api.model.LobstersPostDetails import dev.msfjarvis.claw.model.LobstersPostDetails
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query

View file

@ -1,20 +0,0 @@
package dev.msfjarvis.claw.api.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class Comment(
@Json(name = "short_id") val shortId: String,
@Json(name = "short_id_url") val shortIdUrl: String,
@Json(name = "created_at") val createdAt: String,
@Json(name = "updated_at") val updatedAt: String,
@Json(name = "is_deleted") val isDeleted: Boolean,
@Json(name = "is_moderated") val isModerated: Boolean,
val score: Long,
val flags: Long,
val comment: String,
val url: String,
@Json(name = "indent_level") val indentLevel: Long,
@Json(name = "commenting_user") val user: User,
)

View file

@ -1,10 +0,0 @@
package dev.msfjarvis.claw.api.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class KeybaseSignature(
@Json(name = "kb_username") val kbUsername: String,
@Json(name = "sig_hash") val sigHash: String,
)

View file

@ -1,20 +0,0 @@
package dev.msfjarvis.claw.api.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 submitter: User,
val tags: List<String>,
)

View file

@ -1,21 +0,0 @@
package dev.msfjarvis.claw.api.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class LobstersPostDetails(
@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 submitter: User,
val tags: List<String>,
val comments: List<Comment>,
)

View file

@ -1,19 +0,0 @@
package dev.msfjarvis.claw.api.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class User(
val username: String,
@Json(name = "created_at") val createdAt: String,
@Json(name = "is_admin") val isAdmin: Boolean,
val about: String,
@Json(name = "is_moderator") val isModerator: Boolean,
val karma: Long = 0,
@Json(name = "avatar_url") val avatarUrl: String,
@Json(name = "invited_by_user") val invitedByUser: String,
@Json(name = "github_username") val githubUsername: String? = null,
@Json(name = "twitter_username") val twitterUsername: String? = null,
@Json(name = "keybase_signatures") val keybaseSignatures: List<KeybaseSignature> = emptyList(),
)

View file

@ -1,34 +1,37 @@
package dev.msfjarvis.claw.api package dev.msfjarvis.claw.api
import com.squareup.moshi.Moshi import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dev.msfjarvis.claw.util.TestUtils import dev.msfjarvis.claw.util.TestUtils
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import mockwebserver3.Dispatcher import mockwebserver3.Dispatcher
import mockwebserver3.MockResponse import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer import mockwebserver3.MockWebServer
import mockwebserver3.RecordedRequest import mockwebserver3.RecordedRequest
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.junit.AfterClass import org.junit.AfterClass
import org.junit.BeforeClass import org.junit.BeforeClass
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create import retrofit2.create
@OptIn(ExperimentalSerializationApi::class)
class LobstersApiTest { class LobstersApiTest {
companion object { companion object {
private val contentType = "application/json".toMediaType()
private val webServer = MockWebServer() private val webServer = MockWebServer()
private val moshi = Moshi.Builder().build()
private val okHttp = OkHttpClient.Builder().build() private val okHttp = OkHttpClient.Builder().build()
private val retrofit = private val retrofit =
Retrofit.Builder() Retrofit.Builder()
.client(okHttp) .client(okHttp)
.baseUrl("http://localhost:8080/") .baseUrl("http://localhost:8080/")
.addConverterFactory(MoshiConverterFactory.create(moshi)) .addConverterFactory(Json.asConverterFactory(contentType))
.build() .build()
private val apiClient = retrofit.create<LobstersApi>() private val apiClient = retrofit.create<LobstersApi>()

View file

@ -1,6 +1,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript { buildscript {
val kotlinVersion = "1.5.31"
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@ -12,7 +13,8 @@ buildscript {
} }
dependencies { dependencies {
classpath("com.android.tools:r8:3.1.17-dev") classpath("com.android.tools:r8:3.1.17-dev")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") classpath(kotlin("gradle-plugin", version = kotlinVersion))
classpath(kotlin("serialization", version = kotlinVersion))
classpath("com.android.tools.build:gradle:7.1.0-alpha12") classpath("com.android.tools.build:gradle:7.1.0-alpha12")
classpath("com.diffplug.spotless:spotless-plugin-gradle:5.15.0") classpath("com.diffplug.spotless:spotless-plugin-gradle:5.15.0")
classpath("com.google.dagger:hilt-android-gradle-plugin:2.39") classpath("com.google.dagger:hilt-android-gradle-plugin:2.39")

View file

@ -3,8 +3,7 @@ accompanist = "0.18.0"
aurora = "0.0.54-SNAPSHOT" aurora = "0.0.54-SNAPSHOT"
coroutines = "1.5.2" coroutines = "1.5.2"
hilt = "2.39" hilt = "2.39"
moshix = "0.14.1" serialization = "1.3.0"
retrofit = "2.9.0"
sqldelight = "1.5.1" sqldelight = "1.5.1"
[libraries] [libraries]
@ -12,6 +11,8 @@ sqldelight = "1.5.1"
kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlin-coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" } kotlin-coroutines-jvm = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm", version.ref = "coroutines" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "serialization" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
accompanist-insets = { module = "com.google.accompanist:accompanist-insets", version.ref = "accompanist" } accompanist-insets = { module = "com.google.accompanist:accompanist-insets", version.ref = "accompanist" }
accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" } accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanist" }
@ -35,11 +36,8 @@ dagger-hilt-android = { module = "com.google.dagger:hilt-android", version.ref =
dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } dagger-hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" } dagger-hilt-core = { module = "com.google.dagger:hilt-core", version.ref = "hilt" }
moshi-lib = "com.squareup.moshi:moshi:1.12.0" retrofit-lib = "com.squareup.retrofit2:retrofit:2.9.0"
moshix-ksp = { module = "dev.zacsweers.moshix:moshi-ksp", version.ref = "moshix" } retrofit-kotlinxSerializationConverter = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
retrofit-lib = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-moshiConverter = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
sqldelight-jvmDriver = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-jvmDriver = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" }
sqldelight-androidDriver = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-androidDriver = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }

11
model/build.gradle.kts Normal file
View file

@ -0,0 +1,11 @@
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
}
kotlin {
jvm { compilations.all { kotlinOptions.jvmTarget = "11" } }
sourceSets["commonMain"].apply {
dependencies { implementation(libs.kotlinx.serialization.core) }
}
}

View file

@ -0,0 +1,20 @@
package dev.msfjarvis.claw.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class Comment(
@SerialName("short_id") val shortId: String,
@SerialName("short_id_url") val shortIdUrl: String,
@SerialName("created_at") val createdAt: String,
@SerialName("updated_at") val updatedAt: String,
@SerialName("is_deleted") val isDeleted: Boolean,
@SerialName("is_moderated") val isModerated: Boolean,
val score: Long,
val flags: Long,
val comment: String,
val url: String,
@SerialName("indent_level") val indentLevel: Long,
@SerialName("commenting_user") val user: User,
)

View file

@ -0,0 +1,10 @@
package dev.msfjarvis.claw.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class KeybaseSignature(
@SerialName("kb_username") val kbUsername: String,
@SerialName("sig_hash") val sigHash: String,
)

View file

@ -0,0 +1,20 @@
package dev.msfjarvis.claw.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class LobstersPost(
@SerialName("short_id") val shortId: String,
@SerialName("short_id_url") val shortIdUrl: String,
@SerialName("created_at") val createdAt: String,
val title: String,
val url: String,
val score: Long,
val flags: Long,
@SerialName("comment_count") val commentCount: Long,
val description: String,
@SerialName("comments_url") val commentsUrl: String,
@SerialName("submitter_user") val submitter: User,
val tags: List<String>,
)

View file

@ -0,0 +1,21 @@
package dev.msfjarvis.claw.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class LobstersPostDetails(
@SerialName("short_id") val shortId: String,
@SerialName("short_id_url") val shortIdUrl: String,
@SerialName("created_at") val createdAt: String,
val title: String,
val url: String,
val score: Long,
val flags: Long,
@SerialName("comment_count") val commentCount: Long,
val description: String,
@SerialName("comments_url") val commentsUrl: String,
@SerialName("submitter_user") val submitter: User,
val tags: List<String>,
val comments: List<Comment>,
)

View file

@ -0,0 +1,19 @@
package dev.msfjarvis.claw.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class User(
val username: String,
@SerialName("created_at") val createdAt: String,
@SerialName("is_admin") val isAdmin: Boolean,
val about: String,
@SerialName("is_moderator") val isModerator: Boolean,
val karma: Long = 0,
@SerialName("avatar_url") val avatarUrl: String,
@SerialName("invited_by_user") val invitedByUser: String,
@SerialName("github_username") val githubUsername: String? = null,
@SerialName("twitter_username") val twitterUsername: String? = null,
@SerialName("keybase_signatures") val keybaseSignatures: List<KeybaseSignature> = emptyList(),
)

View file

@ -34,3 +34,5 @@ include(":common")
include(":database") include(":database")
include(":desktop") include(":desktop")
include(":model")