Construyendo la abstracción correcta de la pantalla de Android | de TJ | Noviembre de 2020

Esta publicación está muy inspirada en la publicación de Jesse Wilson del mismo título exacto que revoqué descaradamente. Por favor, léalo aquí.

Este post no es una refutación, sino una serie de yuxtaposiciones; habrá un problema y se compararán dos soluciones al mismo problema sobre las abstracciones que utilizan para resolver ese problema, culminando en una comparación de sistemas de visualización en Android.

Las API RESTful no son las más simples recurso para ser consumido en Android fuera de la caja. Usando el estándar HTTPUrlConnection conduce a muchas repeticiones para actividades básicas comunes como GET solicitudes, y dado que los resultados deben terminar en la interfaz de usuario de una forma u otra, algunos subprocesos deben pasar de llamar a un subproceso en segundo plano y recibir en el subproceso de la interfaz de usuario. Echemos un vistazo a dos soluciones que tratan las abstracciones necesarias de manera diferente.

Vóleibol

El siguiente código se extrae del sitio web para desarrolladores de Android:

val textView = findViewById<TextView>(R.id.text)
// ...

// Instantiate the RequestQueue.
val queue = Volley.newRequestQueue(this)
val url = "https://www.google.com"

// Request a string response from the provided URL.
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<String> { response ->
// Display the first 500 characters of the response string.
textView.text = "Response is: ${response.substring(0, 500)}"
},
Response.ErrorListener { textView.text = "That didn't work!" })

// Add the request to the RequestQueue.
queue.add(stringRequest)

Esta implementación realmente resuelve el problema, pero hay problemas:
1. Especifique el método de solicitud y la URL se volverá muy antigua, muy rápido

2. El uso de devoluciones de llamada para proporcionar respuestas hace que la devolución de llamada sea una eventualidad

3. La biblioteca no se puede configurar fácilmente, encadenar solicitudes y elegir el hilo en el que entregar los resultados es una tarea ardua. ¿Qué sucede si desea entregar resultados a una base de datos, no a la interfaz de usuario?

4. Peor aún, si Volley no admite el tipo que desea, debe escribir un convertidor personalizado para él.

Modernización

El siguiente fragmento se ha eliminado de la página de modificación:

public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit modela la API como una interfaz, y los argumentos del método no contienen nada sobre el tipo de solicitud, los encabezados ni nada más. En su lugar, se especifican en anotaciones, por lo que la persona que llama no necesita ser detallado al usarlo. Además, dado que la API consultada es RESTful, ya está escrita y bien definida. La lógica de la deserialización se puede pasar al generador de modernización y no al sitio de la llamada, la forma en que elija hacerlo (Moshi, Jackson, Gson) es un detalle de implementación que depende de usted. Retrofit le permite describir su API exactamente como es y usarla como tal; Básicamente, no requiere que defina la semántica y los tipos de otro dominio (método HTTP, parámetros, encabezados) directamente en su código.

De manera similar a las API RESTful anteriores, los datos que escribe existen en algún lugar; serializado de manera similar y nos gustaría recuperarlo de manera rápida y conveniente.

Habitación

Los siguientes elementos se extraen del sitio para desarrolladores de Android:

@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>

@Query("SELECT * FROM user WHERE uid IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>

@Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
"last_name LIKE :last LIMIT 1")
fun findByName(first: String, last: String): User

@Insert
fun insertAll(vararg users: User)

@Delete
fun delete(user: User)
}

Este es un enfoque prometedor, de hecho, es muy similar a Retrofit, no requiere un constructor de consultas SQL, ni requiere que pases la consulta de código a algún objeto para analizarlo por ti, en su lugar, representa consultas anotadas.

Sin embargo, para analizar realmente los resultados de la consulta en clases de datos o POJO, primero defina el modelo y luego fuerce la consulta para que coincida. De hecho, en el momento de la compilación, lint advertirá que la consulta puede ser más eficiente seleccionando las columnas que realmente se usarán para completar el modelo, en lugar de la configuración predeterminada para todas las columnas con*. Sin embargo, no tiene el concepto de un adaptador de tipo como Retrofit, donde puede intercambiar la implementación de cómo desea deserializar las consultas. Esto no es necesariamente algo malo, la deserialización de json no es sencilla y, lo que es más importante, a diferencia de una consulta SQL, una solicitud HTTP no tiene información completa del tipo de respuesta implícitamente en su definición, por lo que Room puede tomarse la libertad de Coerción de consultas para modelos.

sqldelight

Los siguientes elementos se extraen de la página github de sqldelight:

CREATE TABLE hockey_player (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
number INTEGER NOT NULL
);

Sqldelight, por otro lado, se da cuenta de que la consulta ya contiene el tipo de modelo y generará el modelo a partir de la consulta. Muchas menos rotondas y sin advertencias imprecisas sobre el rendimiento de las consultas. Mejor aún, si la consulta cambia, el modelo también cambia; una abstracción mejor y más elegante para un problema similar pero bastante diferente.

Puede pensar que sabe a dónde va esto, pero aún puede haber un giro.

El tema común que puede haber notado en los ejemplos anteriores es que generalmente hay un límite que salvar:

  • Una API RESTful para una clase de datos o POJO
  • Una consulta SQL a un POJO o una clase de datos

Por lo general, la información escrita existe en un dominio y debe extraerse para ser utilizada en otro mientras se conserva la ortografía. En Android, la capa de vista se ha representado tradicionalmente en XML, sin embargo, la manipulación de tales vistas debe realizarse en código Kotlin o Java. Se han creado varias abstracciones para abordar este problema:

  • La vista manual busca con findViewById
  • Cuchillo de mantequilla
  • Sintéticos Kotlin
  • Anko
  • ViewBinding
  • Jetpack Compose

findViewById es el menos ergonómico del grupo, análogo a insertar SQL sin formato o crear una conexión HTTPURLC al sitio de uso. En el caso de ButterKnife y Kotlin Synthetics, el primero ha quedado obsoleto y el segundo pronto estará a favor de ViewBinding.

Anko, aunque ahora en desuso, tiene una filosofía compuesta similar a Jetpack; Dado que la lógica empresarial estará en Kotlin, ¿por qué se escribió la vista en un dominio separado? Si bien Anko tenía problemas endémicos (restaurar el estado de la pantalla sin ID de pantalla, por ejemplo), Jetpack compose resuelve la mayoría de ellos, por lo que la verdadera pregunta se reduce al enfoque:

¿Es necesario escribir el dominio View en código?

Personalmente creo que la respuesta a esta pregunta es no, por las siguientes razones:

  1. Abstracciones: Tanto en los casos de redes como de bases de datos, no hay mucha publicidad para escribir código de red o consultas sql en kotlin. Volley hace esto para Android, y dije por qué creo que es una mala idea, y en el lado de SQL, hay una razón por la que nadie habla realmente de JOOQ para Android; Las API RESTful y las consultas SQL se representan mejor en sus formas naturales. Para la manipulación y las transformaciones posteriores, el mejor enfoque fue permitir la generación de tipos para datos estructurados existentes y no representar un dominio único con la semántica de otro. Retrofit, sqlDelight y ViewBinding hacen todo esto con aplomo. Creo que Jetpack compose es muy similar a JOOQ, donde proporciona APIS para representar vistas y sus estilos con Kotlin. Para hacer esto, construcciones de almacenamiento como remember, que duplica la funcionalidad que ya existe en LiveData o StateFlow en ViewModel.
  2. Tratamiento sintomático: Uno de los mayores problemas con el sistema de visualización actual en Android es la dicotomía de estado; los ViewModel es Views cada uno tiene su propio estado, lo que conduce a diferentes fuentes de verdad. La solución para esto es realmente que androidx tiene un paquete de widgets sin estado, donde las vistas son solo eso; apátrida. UNA StatelessEditText es StatelessCheckbox Sería de gran ayuda para resolver los problemas persistentes que surgen cuando se trata de observar y vincular el estado al mismo tiempo que se emiten mutaciones de ese estado desde sus versiones contemporáneas de hoy. Descartar lo existente View crear una gran cantidad de deuda tecnológica de la noche a la mañana es tan valiente como quitar la toma de auriculares o negarse a adoptar USB C en favor de los costosos imanes de hockey. Es emblemático en el tratamiento del síntoma y no de la causa.
  3. Ergonomía: Encuentro la ergonomía de la escritura y las visualizaciones de estilo en XML insuperable, sin embargo, me doy cuenta de que esto varía de un individuo a otro. La implementación actual de usar un ConstraintLayout en Jetpack la composición deja mucho que desear. Incluso la integración de la vista previa del editor de diseño con XML es actualmente incomparable para Compose. Considere también lo siguiente:
Una declaración de vista XML
Extensión de Kotlin para asociar una vista con un objeto de datos

Con un método de extensión, la conexión entre XML y Kotlin y tal vez incluso Java se vuelve trivial. El tipo asociado también podría ser una interfaz de implementación múltiple, lo que permite la portabilidad. Lo mismo bind se puede usar en diferentes contextos de forma independiente y se deben definir diferentes diseños XML para el paisaje o el retrato como siempre lo han sido, y ninguno de estos debe estar representado en el código.

Yendo más allá, realmente no hay razón para que lo anterior no se pueda usar para definir un archivo Composable. UNA composableFrom se podría escribir una extensión que buscaría una definición XML y generaría un archivo Composable de él, sin tener que eliminar completamente XML. Lo más importante que Jetpack Compose hace bien es esto Composables son apátridas; a la función Composable se le debe pasar todo lo que necesita para representar el widget. No hay nada en esto a lo que XML sea antitético.

HTML y CSS han estado abogando por esto durante un tiempo, la separación de la visualización y el estilo de las vistas y su vínculo con los datos que las pueblan. Jetpack Compose es actualmente muy similar a JSX en React y tiene todas las ventajas y desventajas. Creo que está bien que las personas que quieran poder escribir sus propios diseños en código puedan hacerlo directamente, pero Jetpack compose no necesita excluir XML; de hecho, creo que representa una oportunidad perfecta para completarlo y mejorarlo, sin generar mucha deuda tecnológica de la noche a la mañana.

Jetbrains acaba de anunciar el lanzamiento del hito de Jetpack Compose en el escritorio. ¿Sabes qué sería genial? Si las mismas declaraciones de pantalla en Android funcionaron igual de bien en Desktop. ¿Sabes qué podría habilitarlo? Un lenguaje de marcado independiente de la plataforma que era algo extensible…. oh, XML. Las etiquetas XML específicas de Android se pueden cambiar por etiquetas independientes de la plataforma, LinearLayout para VerticalRow es HorizontalRow, ConstraintLayout para ConstraintBlock o algo similar; la lista continua. Esto se vuelve aún más evidente cuando considera que Jetpack Compose todavía usa una instancia de Android View debajo del capó, aunque sea personalizada.

Jetpack Compose es increíble, sin embargo, no se excluye entre sí con XML, de hecho, crea Composables a partir de XML a la ViewBinding podría ser el puente que necesitamos para conectar el pasado de Android con un futuro multiplataforma sin problemas; HTML y CSS para la web, XML para plataformas nativas.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *