Skip to content

Commit

Permalink
Merge pull request #42 from Q42/kotlinx-serialization
Browse files Browse the repository at this point in the history
REPLACE Moshi by Kotlinx serialization
  • Loading branch information
ninovanhooff authored Feb 29, 2024
2 parents 5c779f4 + 59a2635 commit 8ba3443
Show file tree
Hide file tree
Showing 13 changed files with 53 additions and 129 deletions.
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
8 changes: 0 additions & 8 deletions build.dep.json.gradle

This file was deleted.

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
alias libs.plugins.hilt apply false
alias libs.plugins.ksp apply false
}
Expand Down
3 changes: 1 addition & 2 deletions core/actionresult/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -11,4 +10,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 @@ -47,25 +47,31 @@ private fun <T : Any> NetworkResponse<T, ApiErrorResponse>.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)
}
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,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()
Expand Down
3 changes: 1 addition & 2 deletions data/user/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -14,4 +13,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)
@Serializable
internal data class UserDTO(
val args: ArgsDTO
)

@JsonClass(generateAdapter = true)
@Serializable
internal 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 @@ manesVersions = "0.44.0"
littleRobotsCatalogUpdates = "0.8.1"
hilt = "2.50"
retrofit = "2.9.0"
moshi = "1.15.0"
kotlinx-serialization = "1.6.2"
retrofit2KotlinxSerializationConverter = "1.0.0"
networkResponseAdapter = "5.0.0"
napier = "2.7.1"
composeDestinations = "1.9.60"
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

0 comments on commit 8ba3443

Please sign in to comment.