Resolviendo el problema de enumeración de Moshi con genéricos

Contenidos

Cómo los parámetros de tipo cosificados lo salvan de «copias de pasta» al analizar enumeraciones.

Será mejor que usemos un adaptador universal. Foto de Castorly Stock en Pexels. Moshi es una de las bibliotecas de análisis JSON más populares en Android que nos permite convertir un objeto JSON en una clase de datos y viceversa. Funciona bien con Retrofit, una de las bibliotecas de red más populares, y dado que las respuestas del servidor se presentan principalmente en formato JSON, Moshi nos evita escribir código repetitivo cuando las analizamos en el cliente. Pero para los desarrolladores de Android amantes de DRY, Moshi tiene un defecto fatal; enumeraciones. De forma predeterminada, Moshi requiere analizadores personalizados para cada enumeración en su API o necesita representar enumeraciones como cadenas que ninguna base de datos de back-end lo hará como cadenas. Por supuesto que podemos (¡es software!), pero la respuesta puede sorprenderte. Porque todo lo que necesitas es una anotación. Tenemos la siguiente representación JSON de una persona, que se puede convertir automáticamente en una clase de datos simplemente agregando la anotación @JsonClass(generateAdapter = true). Esta anotación genera un adaptador en tiempo de compilación que realiza el análisis por usted, que es el tipo de modelado que queremos lograr. Agreguemos una propiedad adicional en nuestra clase de datos personales que es Rol. Por ahora, sería una clase de enumeración con dos valores posibles, administrador o moderador. Si agrega una clase de enumeración a las propiedades de PersonNow, ¿qué tipo de valor espera en la representación JSON para asignar a un rol? ¿Es un número o una cadena? Moshi admite el análisis de enumeraciones utilizando EnumJsonAdapter, pero hay una trampa. El valor en JSON debe ser una cadena que coincida con el nombre de la constante de enumeración en el cliente. Obviamente, esto hace que el código sea propenso a errores y más difícil de refactorizar, ya que simplemente cambiar el nombre de una constante de enumeración interrumpe el análisis. Uso de EnumJsonAdapter de Moshi para analizar un rol El valor del rol debe coincidir con el nombre de la constante de enumeración en el cliente; ya sea administrador o moderador

¿Qué sucede si usa números para representar los valores en su lugar?

Dado que representar los valores como cadenas es propenso a errores, ¿qué pasa si usamos enteros simples que el cliente analiza y los asigna a la constante de enumeración adecuada? El valor del rol ahora se representa como un número entero. Esto hace que nuestra API sea menos detallada y nuestro código de cliente sea más fácil de mantener, ya que ya no nos permitimos romper el análisis simplemente cambiando el nombre de una constante de enumeración. Sin embargo, ahora no podemos usar el EnumJsonAdapter provisto por Moshi porque los valores no están representados por cadenas. Necesitamos escribir un adaptador personalizado que asigne el entero a una constante de enumeración. Primero, agreguemos una propiedad de valor en nuestra clase de enumeración de roles que coincida con la que definimos en nuestra representación JSON: Asociación de un valor entero con nuestra clase de enumeración de roles Como podemos ver en el fragmento anterior, un administrador ahora tiene un valor de 1 vinculado mientras que un moderador está vinculado con un valor de 2. La función fromValueOrNull toma el valor y lo asigna a una constante de enumeración. Ahora escribamos un adaptador personalizado que realice el mapeo de FromJson y ToJson: un adaptador personalizado de Moshi que asigna un número entero a una constante de enumeración de roles. Pero lo hicimos a costa de escribir un adaptador personalizado. ¿Qué pasa si tenemos docenas de enumeraciones con un valor asociado? ¿Tenemos que escribir todo el código repetitivo cada vez? Bueno, aquí es donde entran los genéricos. En primer lugar, estamos de acuerdo en que debemos asociar nuestras enumeraciones con un número. Esto significa que necesitamos definir una interfaz para que nuestras enumeraciones la implementen: cree una interfaz IEnumValue para que nuestras enumeraciones la implementen, y creamos una GenericEnumFactory con una función fromValueOrNull que toma un número entero y es del tipo T de la enumeración y realiza el mapeo para devolvernos la constante de enumeración. Por supuesto, el parámetro de tipo T debe implementar la interfaz IEnumValue: cree una GenericEnumFactory para evitar especificar una función fromValueOrNull para cada una de nuestras enumeraciones. ¿Podríamos de alguna manera simplemente definir un adaptador y reutilizarlo para todas nuestras enumeraciones?

La magia de los parámetros tipo cosificados

Si queremos crear un adaptador genérico, necesitamos acceso al tipo T de la enumeración para poder acceder a las constantes de enumeración y su valor, que expone la interfaz IEnumValue. Pero sabemos que el tipo se elimina en tiempo de ejecución y solo está disponible en tiempo de compilación. Aquí es donde entran los parámetros de tipo cosificados: nos permiten acceder a un tipo pasado como parámetro como si fuera una clase normal. Sin embargo, al momento de escribir esta publicación, los parámetros de tipo cosificados no son compatibles con Kotlin a nivel de clase, por lo que cualquier intento de crear un adaptador genérico como este fallaría: no se puede acceder a los valores de Enum a menos que se verifique T, que es no se admite en el nivel de clase. Sin embargo, hay otro truco que podemos usar. Sabemos que los parámetros de tipo verificados son compatibles con las funciones en línea. Entonces, ¿qué pasa si en lugar de una clase genérica definimos una función genérica que llama al JsonAdapter genérico? ¿devoluciones? ¡Y esa será una solución perfectamente válida! Una función genérica que devuelve el JsonAdapter genérico¡Allí tenemos una función genérica que genera automáticamente el adaptador de enumeración para que hagamos el mapeo fromJson y toJson y admite la representación entera de nuestras enumeraciones en las respuestas del servidor!

Nota: firstOrNull, como sugiere el nombre, puede devolver un valor nulo si el valor entero que proporcionamos no se puede asignar a una constante de enumeración. Esto significa que las enumeraciones en las clases de datos que modelan las respuestas de nuestro servidor deben ser anulables. Sin embargo, tenemos la opción de definir un valor predeterminado, por ejemplo, usando enumValues().first() si firstOrNull devuelve nulo.

Usar esta característica es bastante simple, solo necesitamos agregar los adaptadores en nuestra definición de Moshi.Builder, como lo haríamos con cualquier otro adaptador de Moshi personalizado: agregue el adaptador personalizado para nuestras enumeraciones en nuestra definición de Moshi.Builder. Este enfoque nos permite representar las enumeraciones como enteros en las respuestas del servidor sin tener que escribir y mantener un adaptador separado para cada una de nuestras clases de enumeración. Al mismo tiempo, el código se vuelve más fácil de mantener y refactorizar y más resistente a los errores. Lucas Cavalcante y Stelios Frantzeskakis son desarrolladores de Perry Street Software, editores de las aplicaciones de citas LGBTQ+ SCRUFF y Jack’d, con más de 30 millones de miembros en todo el mundo.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.