diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0024ff60..62083d19 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ import com.android.sdklib.AndroidVersion.VersionCodes plugins { id("com.android.application") kotlin("android") + kotlin("plugin.serialization") alias(libs.plugins.ksp) id("kotlin-parcelize") id("io.gitlab.arturbosch.detekt") @@ -110,9 +111,9 @@ dependencies { implementation(libs.retrofit) implementation(platform(okhttp.bom)) implementation(okhttp.core) - implementation(libs.retrofit.moshi) - implementation(libs.moshi) - ksp(libs.moshi.codegen) + implementation(libs.retrofit.kotlinx) + implementation(libs.kotlinx.serialization) + implementation(libs.kotlinx.datetime) implementation(libs.timber) implementation(libs.leakcanary) diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Claim.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Claim.kt index 616cb2c7..98d31cf7 100644 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Claim.kt +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Claim.kt @@ -2,16 +2,18 @@ package dev.fobo66.factcheckerassistant.api.models import android.os.Parcelable import androidx.compose.runtime.Immutable -import com.squareup.moshi.JsonClass +import dev.fobo66.factcheckerassistant.util.InstantParceler +import kotlinx.datetime.Instant import kotlinx.parcelize.Parcelize -import java.time.LocalDateTime +import kotlinx.parcelize.TypeParceler +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable @Immutable @Parcelize data class Claim( val text: String, - val claimant: String?, - val claimDate: LocalDateTime?, - val claimReview: List + val claimant: String? = null, + @TypeParceler() val claimDate: Instant? = null, + val claimReview: List = emptyList() ) : Parcelable diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/ClaimReview.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/ClaimReview.kt index 0cc9351b..4ab70c26 100644 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/ClaimReview.kt +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/ClaimReview.kt @@ -2,17 +2,17 @@ package dev.fobo66.factcheckerassistant.api.models import android.os.Parcelable import androidx.compose.runtime.Immutable -import com.squareup.moshi.JsonClass import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable @Immutable @Parcelize data class ClaimReview( val publisher: Publisher, val url: String, val title: String?, - val reviewDate: String?, + val reviewDate: String? = null, val textualRating: String, val languageCode: String ) : Parcelable diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/FactCheckResponse.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/FactCheckResponse.kt index ccf48070..4186db27 100644 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/FactCheckResponse.kt +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/FactCheckResponse.kt @@ -1,8 +1,8 @@ package dev.fobo66.factcheckerassistant.api.models -import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable data class FactCheckResponse( val claims: List?, val nextPageToken: String? diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Publisher.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Publisher.kt index 591396dc..3c5eabfc 100644 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Publisher.kt +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/api/models/Publisher.kt @@ -2,13 +2,13 @@ package dev.fobo66.factcheckerassistant.api.models import android.os.Parcelable import androidx.compose.runtime.Immutable -import com.squareup.moshi.JsonClass import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable -@JsonClass(generateAdapter = true) +@Serializable @Immutable @Parcelize data class Publisher( - val name: String?, - val site: String? + val name: String? = null, + val site: String? = null ) : Parcelable diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/di/ApiModule.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/di/ApiModule.kt index 359ac3ce..a9feabda 100644 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/di/ApiModule.kt +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/di/ApiModule.kt @@ -1,14 +1,14 @@ package dev.fobo66.factcheckerassistant.di -import com.squareup.moshi.Moshi +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dev.fobo66.factcheckerassistant.api.FactCheckApi -import dev.fobo66.factcheckerassistant.util.LocalDateTimeAdapter +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.create import javax.inject.Singleton @@ -18,16 +18,17 @@ object ApiModule { @Provides @Singleton - fun provideMoshi(): Moshi = - Moshi.Builder() - .add(LocalDateTimeAdapter()) - .build() + fun provideJson(): Json = + Json { + ignoreUnknownKeys = true + isLenient = true + } @Provides @Singleton - fun provideFactCheckApi(moshi: Moshi) = Retrofit.Builder() + fun provideFactCheckApi(json: Json) = Retrofit.Builder() .baseUrl("https://factchecktools.googleapis.com") - .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addConverterFactory(json.asConverterFactory("text/json".toMediaType())) .build() .create() } diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimDetails.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimDetails.kt index f29d4859..49efcca2 100644 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimDetails.kt +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimDetails.kt @@ -35,7 +35,7 @@ import dev.fobo66.factcheckerassistant.api.models.ClaimReview import dev.fobo66.factcheckerassistant.api.models.Publisher import dev.fobo66.factcheckerassistant.ui.theme.FactCheckerAssistantTheme import kotlinx.collections.immutable.toImmutableList -import java.time.LocalDateTime +import kotlinx.datetime.Clock @OptIn(ExperimentalFoundationApi::class) @Composable @@ -161,7 +161,7 @@ private fun ClaimDetailsPreview() { Claim( "test", "tester", - LocalDateTime.now(), + Clock.System.now(), listOf( ClaimReview( Publisher("test", "test.com"), diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimsSearch.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimsSearch.kt index 8f46babf..053972d5 100644 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimsSearch.kt +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/ui/list/ClaimsSearch.kt @@ -90,7 +90,7 @@ fun ClaimsSearch( val claimDate = remember { DateUtils.getRelativeTimeSpanString( context, - claim?.claimDate?.toInstant(ZoneOffset.UTC)?.toEpochMilli() + claim?.claimDate?.toEpochMilliseconds() ?: System.currentTimeMillis() ).toString() } diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/util/InstantParceler.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/util/InstantParceler.kt new file mode 100644 index 00000000..8b794976 --- /dev/null +++ b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/util/InstantParceler.kt @@ -0,0 +1,20 @@ +package dev.fobo66.factcheckerassistant.util + +import android.os.Parcel +import kotlinx.datetime.Instant +import kotlinx.parcelize.Parceler + +object InstantParceler : Parceler { + override fun create(parcel: Parcel): Instant? { + val timestamp = parcel.readString() + return if (timestamp != null) { + Instant.parse(timestamp) + } else { + null + } + } + + override fun Instant?.write(parcel: Parcel, flags: Int) { + parcel.writeString(this?.toString()) + } +} diff --git a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/util/LocalDateTimeAdapter.kt b/app/src/main/kotlin/dev/fobo66/factcheckerassistant/util/LocalDateTimeAdapter.kt deleted file mode 100644 index b88aaaa7..00000000 --- a/app/src/main/kotlin/dev/fobo66/factcheckerassistant/util/LocalDateTimeAdapter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.fobo66.factcheckerassistant.util - -import com.squareup.moshi.FromJson -import com.squareup.moshi.ToJson -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -class LocalDateTimeAdapter { - - @ToJson - fun toJson(value: LocalDateTime): String { - return value.format(DateTimeFormatter.ISO_DATE_TIME) - } - - @FromJson - fun fromJson(value: String): LocalDateTime { - return LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME) - } -} diff --git a/app/src/test/kotlin/dev/fobo66/factcheckerassistant/util/LocalDateTimeAdapterTest.kt b/app/src/test/kotlin/dev/fobo66/factcheckerassistant/util/LocalDateTimeAdapterTest.kt deleted file mode 100644 index d01f1a30..00000000 --- a/app/src/test/kotlin/dev/fobo66/factcheckerassistant/util/LocalDateTimeAdapterTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package dev.fobo66.factcheckerassistant.util - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import java.time.LocalDateTime -import java.time.format.DateTimeParseException - -class LocalDateTimeAdapterTest { - private val adapter = LocalDateTimeAdapter() - private val now = LocalDateTime.parse(DEFAULT_TIMESTAMP) - - @Test - fun `parse date`() { - val date = adapter.toJson(now) - assertEquals(DEFAULT_TIMESTAMP, date) - } - - @Test - fun `fail to serialize date`() { - assertThrows { - adapter.fromJson(WRONG_TIMESTAMP) - } - } - - @Test - fun `serialize date`() { - val date = adapter.fromJson(EXPECTED_TIMESTAMP) - assertEquals(now, date) - } - - companion object { - const val DEFAULT_TIMESTAMP = "2023-03-21T12:34:56.789" - const val EXPECTED_TIMESTAMP = "2023-03-21T12:34:56.789Z" - const val WRONG_TIMESTAMP = "2023-03-21T12:34:56.789+01" - } -} diff --git a/build.gradle.kts b/build.gradle.kts index 7b517858..3dfd0b35 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(androidx.plugins.library) apply false alias(androidx.plugins.test) apply false kotlin("android") version libs.versions.kotlin apply false + kotlin("plugin.serialization") version libs.versions.kotlin apply false alias(libs.plugins.ksp) apply false alias(di.plugins.hilt) apply false alias(analysis.plugins.detekt) apply false diff --git a/settings.gradle.kts b/settings.gradle.kts index 4bb1346b..62f91513 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,6 +27,7 @@ dependencyResolutionManagement { library("material", "com.google.android.material:material:1.10.0") library("retrofit", "com.squareup.retrofit2", "retrofit").versionRef("retrofit") library("retrofit.moshi", "com.squareup.retrofit2", "converter-moshi").versionRef("retrofit") + library("retrofit.kotlinx", "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0") library("retrofit.mock", "com.squareup.retrofit2", "retrofit-mock").versionRef("retrofit") library("leakcanary", "com.squareup.leakcanary:leakcanary-android:2.12") library("commonmark", "org.commonmark:commonmark:0.21.0") @@ -34,6 +35,8 @@ dependencyResolutionManagement { library("coil", "io.coil-kt:coil-compose:2.5.0") library("collections", "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.6") library("desugar", "com.android.tools:desugar_jdk_libs:2.0.4") + library("kotlinx.serialization", "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.1") + library("kotlinx.datetime", "org.jetbrains.kotlinx:kotlinx-datetime:0.4.1") library( "coroutines", "org.jetbrains.kotlinx",