diff --git a/README.MD b/README.MD index 00ac8d3..4ad2c25 100644 --- a/README.MD +++ b/README.MD @@ -232,14 +232,9 @@ yet). ### JSON parsing -We use [Moshi](https://github.com/square/moshi) and moshi code generation for all json parsing -because it is fast, modern and has many extensions available, like support for sealed classes. -Make sure to use `@JsonClass(generateAdapter = true)` on your models, it greatly improves parsing -speed. -For date parsing you can use `com.squareup.moshi:moshi-adapters`. To keep the template small, it is -not included yet. - -### Logging +We use [Kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) for all json parsing +because it is fast, modern and has IDE integration (which warns you when you forget to add @Serializable annotations). +It also has multiplatform support, so we can use it in our KMP projects as well. We use Napier because it's usage is close to Timber/Tolbaaken, but Napier supports KMM. diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 38e82b1..26afcce 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -28,82 +28,6 @@ ## END enums -## START moshi - -# JSR 305 annotations are for embedding nullability information. --dontwarn javax.annotation.** - --keepclasseswithmembers class * { - @com.squareup.moshi.* ; -} - --keepclassmembernames @com.squareup.moshi.JsonClass class * extends java.lang.Enum { - ; -} - --keep @com.squareup.moshi.JsonQualifier interface * - -# Enum field names are used by the integrated EnumJsonAdapter. -# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly -# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi. --keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum { - ; - **[] values(); -} - -# The name of @JsonClass types is used to look up the generated adapter. --keepnames @com.squareup.moshi.JsonClass class * - -# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's -# name. We will look this up reflectively to invoke the type's constructor. -# -# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard -# matching preceding parameters. --keepnames class kotlin.jvm.internal.DefaultConstructorMarker --keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * { - synthetic (...); -} - -# Retain generated JsonAdapters if annotated type is retained. --if @com.squareup.moshi.JsonClass class * --keep class <1>JsonAdapter { - (...); - ; -} --if @com.squareup.moshi.JsonClass class **$* --keep class <1>_<2>JsonAdapter { - (...); - ; -} --if @com.squareup.moshi.JsonClass class **$*$* --keep class <1>_<2>_<3>JsonAdapter { - (...); - ; -} --if @com.squareup.moshi.JsonClass class **$*$*$* --keep class <1>_<2>_<3>_<4>JsonAdapter { - (...); - ; -} --if @com.squareup.moshi.JsonClass class **$*$*$*$* --keep class <1>_<2>_<3>_<4>_<5>JsonAdapter { - (...); - ; -} --if @com.squareup.moshi.JsonClass class **$*$*$*$*$* --keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter { - (...); - ; -} - --keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl - --keepclassmembers class kotlin.Metadata { - public ; -} - -## END moshi - ## start OKHTTP for https://github.com/square/okhttp/issues/6258 -dontwarn org.bouncycastle.jsse.BCSSLSocket diff --git a/build.dep.json.gradle b/build.dep.json.gradle deleted file mode 100644 index 50f7c92..0000000 --- a/build.dep.json.gradle +++ /dev/null @@ -1,8 +0,0 @@ -apply { - plugin(libs.plugins.ksp.get().getPluginId()) -} - -dependencies { - implementation libs.moshiKotlin - ksp libs.moshiCodeGen -} \ No newline at end of file diff --git a/build.dep.network.gradle b/build.dep.network.gradle index 2b84290..7d609eb 100644 --- a/build.dep.network.gradle +++ b/build.dep.network.gradle @@ -1,7 +1,12 @@ +apply { + plugin(libs.plugins.kotlinSerialization.get().getPluginId()) +} + dependencies { implementation libs.retrofit - implementation libs.moshiRetrofitConverter + implementation(libs.kotlinx.serialization.json) + implementation libs.retrofit2.kotlinx.serialization.converter implementation libs.networkResponseAdapter implementation libs.okhttp implementation libs.okhttpLogging -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index a9e7ae1..f809ee9 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ plugins { // sets class paths only (because of 'apply false') alias libs.plugins.androidApplication apply false alias libs.plugins.androidLibrary apply false alias libs.plugins.jetbrainsKotlinAndroid apply false + alias libs.plugins.kotlinSerialization apply false alias libs.plugins.hilt apply false alias libs.plugins.ksp apply false } diff --git a/core/actionresult/build.gradle b/core/actionresult/build.gradle index dfd9b13..3ad967e 100644 --- a/core/actionresult/build.gradle +++ b/core/actionresult/build.gradle @@ -2,7 +2,6 @@ plugins { id "com.android.library" } apply from: "$rootDir/build.module.library.gradle" -apply from: "$rootDir/build.dep.json.gradle" apply from: "$rootDir/build.dep.network.gradle" android { @@ -11,4 +10,4 @@ android { dependencies { implementation libs.networkResponseAdapter -} \ No newline at end of file +} diff --git a/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ActionResultMapper.kt b/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ActionResultMapper.kt index b7a6a6c..97f144a 100644 --- a/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ActionResultMapper.kt +++ b/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ActionResultMapper.kt @@ -1,9 +1,9 @@ package nl.q42.template.actionresult.data import com.haroldadmin.cnradapter.NetworkResponse -import com.squareup.moshi.JsonEncodingException import io.github.aakira.napier.Napier import kotlinx.coroutines.CancellationException +import kotlinx.serialization.SerializationException import nl.q42.template.actionresult.domain.ActionResult import java.io.EOFException import java.io.IOException @@ -47,25 +47,31 @@ private fun NetworkResponse.networkResponseToActi } is NetworkResponse.NetworkError -> // Used to represent connectivity errors - when (this.error) { + { + when (val error = this.error) { is UnknownHostException, is ConnectException, is SocketTimeoutException -> { - ActionResult.Error.NetworkError(this.error) + ActionResult.Error.NetworkError(error) } - is JsonEncodingException, is EOFException -> { - // let's log this error, includes a corrupt/broken json response: - ActionResult.Error.Other(this.error) + is EOFException -> { + // let's log this error, includes an incomplete json response + ActionResult.Error.Other(error) } - else -> ActionResult.Error.Other(this.error) + else -> ActionResult.Error.Other(error) } - + } is NetworkResponse.UnknownError -> { val statusCode = this.code val errorMessage = "Received NetworkResponse.UnknownError with response code $statusCode and header ${this.headers}" - val exception = IOException(errorMessage, this.error) - Napier.w(this.error) { "NetworkResponse.UnknownError" } + val error = this.error + val exception = IOException(errorMessage, error) + Napier.w(error) { "NetworkResponse.UnknownError" } when { + error is SerializationException -> { // (usually json) parsing error + ActionResult.Error.InvalidErrorResponse(error) + } + statusCode == null -> { ActionResult.Error.InvalidErrorResponse(exception) } diff --git a/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ApiErrorResponse.kt b/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ApiErrorResponse.kt index 7affd07..7c63b11 100644 --- a/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ApiErrorResponse.kt +++ b/core/actionresult/src/main/kotlin/nl/q42/template/actionresult/data/ApiErrorResponse.kt @@ -1,6 +1,7 @@ package nl.q42.template.actionresult.data -import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + /** * ErrorResponse used by NetworkResponseAdapter. Config it to match your server's error @@ -8,5 +9,5 @@ import com.squareup.moshi.JsonClass * * More info: https://haroldadmin.github.io/NetworkResponseAdapter/ */ -@JsonClass(generateAdapter = true) -data class ApiErrorResponse(val message: String) \ No newline at end of file +@Serializable +data class ApiErrorResponse(val message: String) diff --git a/core/network/build.gradle b/core/network/build.gradle index 3863b3c..ebd642f 100644 --- a/core/network/build.gradle +++ b/core/network/build.gradle @@ -6,4 +6,4 @@ apply from: "$rootDir/build.dep.network.gradle" android { namespace = "nl.q42.template.core.network" -} \ No newline at end of file +} diff --git a/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt b/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt index 5a87d64..800babd 100644 --- a/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt +++ b/core/network/src/main/kotlin/nl/q42/template/core/network/di/NetworkModule.kt @@ -1,16 +1,17 @@ package nl.q42.template.core.network.di import com.haroldadmin.cnradapter.NetworkResponseAdapterFactory -import com.squareup.moshi.Moshi +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import nl.q42.template.core.network.logger.JsonFormattedHttpLogger +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -18,11 +19,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal class NetworkModule { - @Provides - @Singleton - fun providesMoshi(): Moshi = Moshi.Builder() - .build() - @Provides @Singleton fun providesOkhttpClient( @@ -43,12 +39,16 @@ internal class NetworkModule { @Provides fun provideRetrofit( httpClient: OkHttpClient, - moshi: Moshi, @ConfigApiMainPath apiMainPath: String, ): Retrofit { + val contentType = "application/json".toMediaType() + + // When the server adds new fields to the response, we don't want to crash + val json = Json { ignoreUnknownKeys = true } + return Retrofit.Builder() .baseUrl(apiMainPath) - .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addConverterFactory(json.asConverterFactory(contentType)) .addCallAdapterFactory(NetworkResponseAdapterFactory()) .client(httpClient) .build() diff --git a/data/user/build.gradle b/data/user/build.gradle index 270829e..b79cfbf 100644 --- a/data/user/build.gradle +++ b/data/user/build.gradle @@ -4,7 +4,6 @@ plugins { apply from: "$rootDir/build.module.library.gradle" apply from: "$rootDir/build.dep.network.gradle" -apply from: "$rootDir/build.dep.json.gradle" android { namespace = "nl.q42.template.data.user" @@ -14,4 +13,4 @@ dependencies { implementation project(':domain:user') implementation project(':core:network') implementation project(':core:actionresult') -} \ No newline at end of file +} diff --git a/data/user/src/main/kotlin/nl/q42/template/data/user/remote/model/UserDTO.kt b/data/user/src/main/kotlin/nl/q42/template/data/user/remote/model/UserDTO.kt index ded4478..24c06df 100644 --- a/data/user/src/main/kotlin/nl/q42/template/data/user/remote/model/UserDTO.kt +++ b/data/user/src/main/kotlin/nl/q42/template/data/user/remote/model/UserDTO.kt @@ -1,18 +1,19 @@ package nl.q42.template.data.user.remote.model -import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + /** * Data Transfer Models (DTO) are preferably generated from server source code/json/schemas and do not contain * any changes compared to the server contract. */ -@JsonClass(generateAdapter = true) +@Serializable internal data class UserDTO( val args: ArgsDTO ) -@JsonClass(generateAdapter = true) +@Serializable internal data class ArgsDTO( val email: String -) \ No newline at end of file +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fbbb482..238383e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,8 @@ littleRobotsCatalogUpdates = "0.8.1" hilt = "2.49" ksp = "1.9.21-1.0.16" retrofit = "2.9.0" -moshi = "1.15.0" +kotlinx-serialization = "1.6.2" +retrofit2KotlinxSerializationConverter = "1.0.0" networkResponseAdapter = "5.0.0" napier = "2.6.1" composeDestinations = "1.9.55" @@ -34,9 +35,8 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t mockk-agent = { module = "io.mockk:mockk-agent", version.ref = "mockkAndroid" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockkAndroid" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } -moshiRetrofitConverter = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } -moshiKotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } -moshiCodeGen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } networkResponseAdapter = { module = "com.github.haroldadmin:NetworkResponseAdapter", version.ref = "networkResponseAdapter" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttpLogging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } @@ -58,6 +58,7 @@ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } [plugins] androidApplication = { id = "com.android.application", version.ref = "gradlePlugin" } androidLibrary = { id = "com.android.library", version.ref = "gradlePlugin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } manesVersions = { id = "com-github-ben-manes-versions", version.ref = "manesVersions" } littleRobotsCatalogUpdates = { id = "nl.littlerobots.version-catalog-update", version.ref = "littleRobotsCatalogUpdates" }