Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

REPLACE Moshi by Kotlinx serialization #42

Merged
merged 8 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
76 changes: 0 additions & 76 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -28,82 +28,6 @@

## END enums

## START moshi

# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**

-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}

-keepclassmembernames @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
}

-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 {
<fields>;
**[] 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 <init>(...);
}

# Retain generated JsonAdapters if annotated type is retained.
-if @com.squareup.moshi.JsonClass class *
-keep class <1>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*
-keep class <1>_<2>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*
-keep class <1>_<2>_<3>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*
-keep class <1>_<2>_<3>_<4>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
<init>(...);
<fields>;
}

-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl

-keepclassmembers class kotlin.Metadata {
public <methods>;
}

## END moshi

## start OKHTTP for https://github.com/square/okhttp/issues/6258

-dontwarn org.bouncycastle.jsse.BCSSLSocket
Expand Down
5 changes: 0 additions & 5 deletions build.dep.json.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
apply {
plugin(libs.plugins.ksp.get().getPluginId())
ninovanhooff marked this conversation as resolved.
Show resolved Hide resolved
}

dependencies {
implementation libs.moshiKotlin
ksp libs.moshiCodeGen
}
9 changes: 7 additions & 2 deletions build.dep.network.gradle
Original file line number Diff line number Diff line change
@@ -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
}
}
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
ninovanhooff marked this conversation as resolved.
Show resolved Hide resolved
alias libs.plugins.hilt apply false
alias libs.plugins.ksp apply false
}
Expand Down
2 changes: 1 addition & 1 deletion core/actionresult/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ android {

dependencies {
implementation libs.networkResponseAdapter
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -52,20 +52,23 @@ private fun <T : Any> NetworkResponse<T, ApiErrorResponse>.networkResponseToActi
ActionResult.Error.NetworkError(this.error)
}

is JsonEncodingException, is EOFException -> {
// let's log this error, includes a corrupt/broken json response:
is EOFException -> {
// let's log this error, includes an incomplete json response
ActionResult.Error.Other(this.error)
}

else -> ActionResult.Error.Other(this.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" }
when {
this.error is SerializationException -> { // (usually json) parsing error
ActionResult.Error.InvalidErrorResponse(this.error as SerializationException)
ninovanhooff marked this conversation as resolved.
Show resolved Hide resolved
}

statusCode == null -> {
ActionResult.Error.InvalidErrorResponse(exception)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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
* json structure.
*
* More info: https://haroldadmin.github.io/NetworkResponseAdapter/
*/
@JsonClass(generateAdapter = true)
data class ApiErrorResponse(val message: String)
@Serializable
data class ApiErrorResponse(val message: String)
2 changes: 1 addition & 1 deletion core/network/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ apply from: "$rootDir/build.dep.network.gradle"

android {
namespace = "nl.q42.template.core.network"
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
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

@Module
@InstallIn(SingletonComponent::class)
internal class NetworkModule {

@Provides
@Singleton
fun providesMoshi(): Moshi = Moshi.Builder()
.build()

@Provides
@Singleton
fun providesOkhttpClient(
Expand All @@ -43,12 +39,15 @@ internal class NetworkModule {
@Provides
fun provideRetrofit(
httpClient: OkHttpClient,
moshi: Moshi,
@ConfigApiMainPath apiMainPath: String,
): Retrofit {
val contentType = "application/json".toMediaType()

val json = Json { ignoreUnknownKeys = true }
ninovanhooff marked this conversation as resolved.
Show resolved Hide resolved

return Retrofit.Builder()
.baseUrl(apiMainPath)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addConverterFactory(json.asConverterFactory(contentType))
.addCallAdapterFactory(NetworkResponseAdapterFactory())
.client(httpClient)
.build()
Expand Down
2 changes: 1 addition & 1 deletion data/user/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ dependencies {
implementation project(':domain:user')
implementation project(':core:network')
implementation project(':core:actionresult')
}
}
Original file line number Diff line number Diff line change
@@ -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)
internal data class UserDTO(
ninovanhooff marked this conversation as resolved.
Show resolved Hide resolved
@Serializable
data class UserDTO(
val args: ArgsDTO
)

@JsonClass(generateAdapter = true)
internal data class ArgsDTO(
@Serializable
data class ArgsDTO(
val email: String
)
)
9 changes: 5 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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" }
Expand All @@ -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" }
Expand Down