Per al desenvolupament de qualsevol aplicació és necessari utilitzar JSON per comunicar-nos amb el backend i, fins ara, l’habitual era ajudar-se de la llibreria Gson. Tot i que aquesta eina funciona adequadament en Java, no és la millor opció per a Kotlin, ja que poden aparèixer errors no controlats. En els últims temps s’han desenvolupat altres eines que permeten fer el mateix, però són més segures davant aquest tipus de fallades. Estem parlant de Jackson, Kotlinx.Serialization i Moshi. Encara que aquestes tres opcions presenten els mateixos avantatges enfront de Gson, ens centrarem a Moshi per ser la més popular i la més estable actualment.
Què és Moshi?
Moshi és una llibreria per parsejar JSON en objectes Java o Kotlin. Està desenvolupada per Square per gairebé les mateixes persones que Gson. Els principals desenvolupadors, Jesse Wilson i Jake Wharton, han dit que Moshi podria considerar Gson v3.
Els principals avantatges enfront de Gson són:
- Moshi entén i funciona correctament amb els tipus no nuls de Kotlin.
- ja no cal indicar-li a Proguard que no ofusqui el package on tingueu ubicats els vostres models, ja que amb la configuració bàsica és suficient.
- Moshi segueix en desenvolupament i millorant, mentre que Gson és pràcticament una llibreria acabada.
- Ocupa menys mida al Apk.
** (Jesse Wilson exposa més avantatges aquí).
Si esteu acostumats a fer servir Gson no tindreu problemes a usar Moshi, ja que s’assemblen molt. No obstant això, cal tenir en compte algunes diferències i prendre certes precaucions a l’hora de realitzar una migració.
Ús de la llibreria
Es recomana usar Moshi juntament codegen: https://github.com/square/moshi#codegen
implementation "com.squareup.moshi:moshi:$moshi_version"kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
I mai oblidar afegir les rules de Proguard: https://github.com/square/moshi/blob/master/moshi/src/main/resources/ME …
Per a usar-lo juntament amb Retrofit usar aquest converter: https://github.com/square/retrofit/tree/master/retrofit-converters/moshi
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
I afegir a vostra instància de Retrofit de la següent manera:
Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create()) .build()
Amb això ja teniu el vostre projecte llest per a usar Moshi per parsejar vostres JSON.
diferències amb Gson: models
On amb Gson teníem:
data class FooDTO( val id: Int, @SerializedName("nombre") val name: String)
amb Moshi tenim:
@JsonClass(generateAdapter = true)data class FooDTO( val id: Int, @Json(name = "nombre") val name: String)
L’anotació @SerializedName
canvia per @Json
i pel fet que fem servir codegen, cal anotar totes les classes amb @JsonClass(generateAdapter = true
.
Digues cies amb Gson: Deserializer / Serializer
Amb Gson es posaria d’aquesta manera:
class DateAdapter : JsonDeserializer<Date>, JsonSerializer<Date> { private val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) override fun serialize( src: Date, typeOfSrc: Type, context: JsonSerializationContext ): JsonElement { return JsonPrimitive(df.format(src)) } override fun deserialize( json: JsonElement, typeOfT: Type, context: JsonDeserializationContext ): Date { return df.parse(json.asString)!! }}
I amb Moshi d’aquesta altra:
class DateAdapter { private val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()) @ToJson fun toJson(value: Date): String { return df.format(value) } @FromJson fun fromJson(source: String): Date { return df.parse(source)!! }}
Diferències amb Gson: Kotlin null safety
Gson no entén els tipus no nuls de Kotlin. Això vol dir que si intentem deserializar un valor nul en un tipus no nullable, Gson ho farà sense errors, la qual cosa pot causar excepcions inesperades fàcilment. Per exemple:
Atès l’anterior model FooDTO, i un json { “id”: 0, “name”: null}:
- Gson crearà l’objecte FooDTO (0 , null), encara que això no hauria de ser possible ja que name s’ha definit com no nullable. No llançarà cap excepció però si que obtindreu errors a l’hora d’usar name:
- name.isEmpty () produirà un NullPointerException
- name.trim () llançarà un TypeCastException: null can not be cat to non-null type kotlin.CharSequence
- Moshi llançarà la següent excepció a l’deserializar:
com.squareup.moshi.JsonDataException: Required value 'name' (JSON name 'nombre') missing
amb Moshi sabrem des del primer moment que és el que falla en la definició del nostre model, però amb Gson no serem conscients fins al moment d’usar la variable, podent ocórrer en qualsevol part de el codi i podent llançar diversos tipus d’error segons l’ús de la variable i tot això passarà mentre us pregunteu com ha estat possible que una variable no nul·la sigui nul·la.
Migració des Gson
Com que Moshi és més rigorós nO es recomana fer una migració massiva de tots els objectes que tingueu en Java o Kotlin ja que és possible que els vostres models no estiguin ben definits.
Per exemple, si els vostres models tenen definides variables no nullables que en algun moment són nul·les però mai s’han usat, amb Gson mai us haurà saltat l’error i aquest haurà quedat ocult però si aquest mateix model el migráis a Moshi l’error saltarà a l’deserializar. Tot i així hi ha diverses opcions:
- Si el vostre model està en Java: convertir-lo a Kotlin amb tots els seus atributs nullables.
- Els vostres models estan en Kotlin i el codi que els fa servir també està en Kotlin: eliminar els atributs que no es facin servir o fer-los tots nullables.
- Realitzar una migració progressiva: començar a utilitzar Moshi al parseo de noves peticions i deixar les antigues amb Gson, però tenint en compte que no s’han de barrejar dins d’un mateix model.
- realitzar tests als vostres models per així estar 100% segurs que estan ben definits i així migrar sense por a trencar res.
Migració progressiva
Mitjançant aquesta solució tindrem una anotació amb la qual li direm a Retrofit quines són les trucades que volem parsejar amb Moshi.
@Target(AnnotationTarget.FUNCTION)@Retention(RUNTIME)annotation class Moshiinterface GdaxApi { @GET("products") fun products(): List<Product> @Moshi @GET("products") fun productsMoshi(): List<Product>}
perquè això funcioni hem de crear la nostra pròpia Converter.Factory per que comprovi si un servei està anotat amb @Moshi:
class MoshiMigrationConverter(private val moshiConverterFactory: MoshiConverterFactory) : Converter.Factory() { override fun responseBodyConverter( type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? { for (annotation in annotations) { if (annotation.annotationClass == Moshi::class) { return moshiConverterFactory.responseBodyConverter(type, annotations, retrofit) } } return null } override fun requestBodyConverter( type: Type, parameterAnnotations: Array<Annotation>, methodAnnotations: Array<Annotation>, retrofit: Retrofit): Converter<*, RequestBody>? { for (annotation in methodAnnotations) { if (annotation.annotationClass == Moshi::class) { return moshiConverterFactory.requestBodyConverter( type, parameterAnnotations, methodAnnotations, retrofit) } } return null }}
Finalment l’inserim a la nostra instància de Retrofit:
val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiMigrationConverter(MoshiConverterFactory.create())) .addConverterFactory(GsonConverterFactory.create()) .build()
D’aquesta manera els nous serveis que aneu implementant en les vostres apps poden usar Moshi i els antics seguir usant Gson i també permet anar migrant 1-1 vostres s erveis.
Migració amb tests
El primer pas seria fer un test per comprovar que la definició del nostre model es correspon al que retorna el servidor. Per a això únicament necessitarem JUnit i un arxiu .json que sigui la còpia del que ens retorna el servei. Posem el nostre anterior model FooDTO i afegim el següent “foo.json” dins la carpeta d’resources dels test, quedaria en “test / resources / foo.json”
{ "id": 0, "name": "SDOS" }
La nostra classe de test quedaria així:
class FooDTOTest { private val loader = javaClass.classLoader!! private val gson = GsonBuilder().create() @Test fun parse() { val jsonString = String(loader.getResourceAsStream("foo.json").readBytes()) val actual = gson.fromJson(jsonString, FooDTO::class.java) val expected = FooDTO(10, "SDOS") assertEquals(expected, actual) }}
La variable gson deu ser igual a que el feu servir en Retrofit, en el cas de no tingueu cap GsonBuilder (). create () és la que es crea per defecte.
el que estem fent és llegir el fitxer “foo.json”, li diem a Gson que ho deserialize en FooDTO i el resultat el guardem en la variable actual. En expected vam crear un objecte que esperem que sigui el resultat de deserializar el json i finalment vam comprovar amb JUnit que tots dos models són iguals.
Un cop tinguem els nostres tests ja podem migrar FooDTO per usar-lo amb Moshi i la nostra classe de test quedaria així:
class FooDTOTest { private val loader = javaClass.classLoader!! private val moshi = Moshi.Builder().build() @Test fun parse() { val jsonString = String(loader.getResourceAsStream("foo.json").readBytes()) val actual = moshi.adapter(FooDTO::class.java).fromJson(jsonString) val expected = FooDTO(10, "SDOS") assertEquals(expected, actual) }}
a l’igual que abans, moshi deu ser igual a la que feu servir a Retrofit i Moshi.Builder (). build () és la qual es crea per defecte. La idea d’el test segueix sent la mateixa, però ara fem servir Moshi per deserializar el json.
Per tant, com més tests amb diferents tipus de respostes del vostre servidor tingueu millor i molt probablement pugueu descobrir errors en els vostres models abans de migrar a Moshi.
Conclusions
L’arribada de Kotlin ha deixat en evidència algunes mancances de Gson, que poden ser solucionades utilitzant noves llibreries, com Moshi. Moshi és null safety pel que obliga a tenir models ben definits, evitant així l’aparició d’errors inesperats. S’utilitza de manera similar a Gson, però evita afegir excepcions a Proguard. A més, pot coexistir amb Gson i fer-se una migració progressiva.
Gson ha estat un bon company de viatge però és hora d’utilitzar altres alternatives. Ara teniu les eines bàsiques per provar Moshi i comprovar els seus avantatges per vosaltres mateixos.
Esperem que us hagi resultat interessant el nostre post sobre Moshi. Ho aneu a utilitzar o preferiu altres llibreries? Deixa’ns un comentari amb la teva experiència o opinió a l’això!