Explicación detallada de Jetpack Compose | Optimizar la construcción de la interfaz de usuario

Las expectativas de las personas para el desarrollo de la interfaz de usuario han cambiado. Hoy en día, para satisfacer las necesidades de los usuarios, las aplicaciones que construimos deben incluir una interfaz de usuario completa, que debe incluir animación y movimiento, requisitos que no existían cuando se creó el kit de herramientas de UI. Para resolver el problema técnico de cómo crear rápida y eficientemente una interfaz de usuario completa, presentamos Jetpack Compose, un conjunto de herramientas de interfaz de usuario moderno que puede ayudar a los desarrolladores a tener éxito en la nueva tendencia.

 

En dos artículos de esta serie, explicaremos las ventajas de Compose y exploraremos los principios de funcionamiento detrás de él. Como apertura, en este artículo, compartiré los problemas que resuelve Compose, las razones detrás de algunas decisiones de diseño y cómo estas decisiones ayudan a los desarrolladores. Además, compartiré el modelo de pensamiento de Compose, cómo debería considerar escribir código en Compose y cómo crear su propia API.

Problemas resueltos con redactar

La separación de preocupaciones (SOC) es un principio de diseño de software bien conocido, que es uno de los conocimientos básicos que debemos aprender como desarrolladores. Sin embargo, aunque es ampliamente conocido, en la práctica a menudo es difícil comprender si se debe seguir el principio. Ante tales problemas, puede resultar útil considerar este principio desde la perspectiva del "acoplamiento" y la "cohesión".

 

Al escribir código, creamos módulos con múltiples unidades. "Acoplamiento" es la dependencia entre unidades en diferentes módulos, que refleja cómo cada parte de un módulo afecta a cada parte de otro módulo. "Cohesión" se refiere a la relación entre las unidades de un módulo e indica el grado razonable de combinación mutua de las unidades del módulo.

 

Al escribir software mantenible, nuestro objetivo es minimizar el acoplamiento y aumentar la cohesión .

Cuando tratamos con módulos estrechamente acoplados , cambiar el código en un lugar significa hacer muchos otros cambios en otros módulos. Para empeorar las cosas, el acoplamiento suele ser implícito, por lo que las modificaciones aparentemente no relacionadas pueden causar errores inesperados.

 

La separación de preocupaciones es organizar los códigos relacionados juntos tanto como sea posible para que podamos mantenerlos fácilmente y facilitar nuestra expansión de nuestro código a medida que crece la escala de la aplicación.

 

Hagamos operaciones más prácticas en el contexto del desarrollo actual de Android y tomemos el modelo de vista y el diseño XML como ejemplos:

El modelo de vista proporciona datos al diseño. Resulta que hay muchas dependencias ocultas aquí: hay muchos acoplamientos entre el modelo de vista y el diseño. Una forma más familiar de permitirle ver esta lista es a través de alguna API, como findViewByID. El uso de estas API requiere una cierta comprensión de la forma y el contenido del diseño XML.

 

Para utilizar estas API, debe comprender cómo se define el diseño XML y cómo se acopla con el modelo de vista. Dado que la escala de aplicaciones crecerá con el tiempo, también debemos asegurarnos de que estas dependencias no se vuelvan obsoletas.

 

La mayoría de las aplicaciones modernas mostrarán dinámicamente la interfaz de usuario y continuarán evolucionando durante la ejecución. Como resultado, la aplicación no solo necesita verificar que el diseño XML satisfaga estas dependencias estáticamente, sino que también debe asegurarse de que estas dependencias se cumplan durante el ciclo de vida de la aplicación. Si un elemento abandona la jerarquía de vistas en tiempo de ejecución, algunas dependencias pueden romperse y causar problemas como NullReferenceExceptions.

Por lo general, el modelo de vista se define en un lenguaje de programación como Kotlin y el diseño está en XML. Debido a las diferencias entre los dos idiomas, existe una línea de separación obligatoria entre ellos. Sin embargo, incluso en este caso, el modelo de vista y el XML de diseño pueden estar estrechamente relacionados. En otras palabras, están estrechamente acoplados.

 

Esto plantea la pregunta: ¿Qué pasa si comenzamos a definir el diseño y la estructura de la interfaz de usuario en el mismo idioma? ¿Qué pasa si elegimos a Kotlin para hacer esto?

Dado que podemos usar el mismo lenguaje, algunas dependencias implícitas en el pasado pueden volverse más obvias. También podemos refactorizar el código y moverlo a lugares donde puedan reducir el acoplamiento y aumentar la cohesión.

Ahora, podría pensar que esta es una sugerencia de que mezcle la lógica con la interfaz de usuario. Pero la realidad es que no importa cómo organice la estructura, habrá lógica asociada con la interfaz de usuario en su aplicación. El marco en sí no cambia esto.

 

Sin embargo, el marco puede proporcionarle algunas herramientas para ayudarlo a lograr la separación de inquietudes más fácilmente: esta herramienta es la función Composable. Durante mucho tiempo, ha utilizado el método para lograr la separación de inquietudes en otras partes del código. Está haciendo este tipo de La refactorización y las habilidades adquiridas al escribir código conciso, confiable y fácil de mantener se pueden aplicar a las funciones de Composable.

Anatomía de la función compostable

Este es un ejemplo de una función Composable:

@Composable
fun App(appData: AppData) {
  val derivedData = compute(appData)
  Header()
  if (appData.isOwner) {
    EditButton()
  }
  Body {
    for (item in derivedData.items) {
      Item(item)
    }
  }
}

En el ejemplo, la función recibe datos de la clase AppData como parámetros. Idealmente, estos datos son datos inmutables y la función Composable no cambiará: la función Composable debería convertirse en una función de conversión para estos datos. De esta manera, podemos usar cualquier código Kotlin para obtener estos datos y usarlo para describir nuestra estructura jerárquica, como las llamadas Header () y Body ().

 

Esto significa que llamamos a otras funciones de Composable, y estas llamadas representan la interfaz de usuario en nuestra jerarquía. Podemos usar primitivas de nivel de lenguaje en Kotlin para realizar dinámicamente varias operaciones. También podemos usar declaraciones if y bucles for para implementar el flujo de control para manejar una lógica de interfaz de usuario más compleja.

 

Las funciones componibles generalmente usan la sintaxis lambda final de Kotlin, por lo que Body () es una función componible con un parámetro lambda componible. Esta relación implica jerarquía o estructura, por lo que aquí Body () puede contener una colección de múltiples elementos.

IU declarativa

"Declarativo" es una palabra de moda, pero también es una palabra muy importante. Cuando hablamos de programación declarativa, estamos hablando de lo opuesto a la programación imperativa. Veamos un ejemplo:

 

Suponga que hay una aplicación de correo electrónico con un icono de mensaje no leído. Si no hay mensaje, la aplicación dibujará un sobre vacío; si hay algunos mensajes, dibujaremos papel en el sobre; y si hay 100 mensajes, dibujaremos el icono como si estuviera en llamas ...

Usando la interfaz imperativa, podríamos escribir una función con la siguiente cantidad de actualización:

fun updateCount(count: Int) {
  if (count > 0 && !hasBadge()) {
    addBadge()
  } else if (count == 0 && hasBadge()) {
    removeBadge()
  }
  if (count > 99 && !hasFire()) {
    addFire()
    setBadgeText("99+")
  } else if (count <= 99 && hasFire()) {
    removeFire()
  }
  if (count > 0 && !hasPaper()) {
   addPaper()
  } else if (count == 0 && hasPaper()) {
   removePaper()
  }
  if (count <= 99) {
    setBadgeText("$count")
  }
}

En este código, recibimos la nueva cantidad y debemos averiguar cómo actualizar la interfaz de usuario actual para reflejar el estado correspondiente. Aunque es un ejemplo relativamente simple, todavía hay muchos casos extremos y la lógica aquí no es simple.

 

En cambio, escribir esta lógica usando una interfaz declarativa se vería así:

@Composable
fun BadgedEnvelope(count: Int) {
  Envelope(fire=count > 99, paper=count > 0) {
    if (count > 0) {
      Badge(text="$count")
    }
  }
}

Aquí definimos:

  • Cuando el número es mayor que 99, muestra la llama;

  • Cuando la cantidad sea mayor que 0, muestre papel;

  • Cuando la cantidad es mayor que 0, dibuja burbujas de cantidad.

Este es el significado de API declarativa. Escribimos código para describir la interfaz de usuario de acuerdo con nuestras ideas, no cómo hacer la transición al estado correspondiente. La clave aquí es que al escribir un código declarativo como este, no necesita prestar atención al estado en el que se encontraba su interfaz de usuario antes, sino solo especificar el estado en el que debería estar. El marco controla cómo pasar de un estado a otro, por lo que ya no necesitamos pensar en ello.

Combinación vs herencia

En el campo del desarrollo de software, la composición se refiere a cómo se combinan múltiples unidades de código simple para formar una unidad de código más compleja. En el modelo de programación orientada a objetos, una de las formas más comunes de composición es la herencia basada en clases. En el mundo de Jetpack Compose, debido a que usamos funciones en lugar de tipos, la forma de lograr la composición es bastante diferente, pero también tiene muchas ventajas en comparación con la herencia. Veamos un ejemplo:

 

Supongamos que tenemos una vista y queremos agregar una entrada. En el modelo de herencia, nuestro código podría verse así:

class Input : View() { /* ... */ }
class ValidatedInput : Input() { /* ... */ }
class DateInput : ValidatedInput() { /* ... */ }
class DateRangeInput : ??? { /* ... */ }

View es la clase base y ValidatedInput usa una subclase de Input. Para validar la fecha, DateInput usa una subclase de ValidatedInput. Pero luego viene el desafío: tenemos que crear una entrada de rango de fechas, lo que significa que se deben validar dos fechas: fechas de inicio y finalización. Puede heredar DateInput, pero no puede ejecutarlo dos veces.Esta es la limitación de la herencia: solo podemos heredar de una clase principal.

 

En Compose, este problema se vuelve muy simple. Supongamos que comenzamos con una función básica de Input Composable:

@Composable
fun <T> Input(value: T, onChange: (T) -> Unit) { 
  /* ... */
}

Cuando creamos ValidatedInput, solo necesitamos llamar a Input en el cuerpo del método. Luego podemos decorarlo para implementar la lógica de verificación:

@Composable
fun ValidatedInput(value: T, onChange: (T) -> Unit, isValid: Boolean) { 
  InputDecoration(color=if(isValid) blue else red) {
    Input(value, onChange)
  }
}

A continuación, para DataInput, podemos llamar directamente a ValidatedInput:

@Composable
fun DateInput(value: DateTime, onChange: (DateTime) -> Unit) { 
  ValidatedInput(
    value,
    onChange = { ... onChange(...) },
    isValid = isValidDate(value)
  )
}

Ahora, cuando implementamos la entrada de rango de fechas, ya no hay ningún desafío aquí: solo necesita ser llamado dos veces. Los ejemplos son los siguientes:

@Composable
fun DateRangeInput(value: DateRange, onChange: (DateRange) -> Unit) { 
  DateInput(value=value.start, ...)
  DateInput(value=value.end, ...)
}

En el modelo de composición de Compose, ya no tenemos la limitación de una sola clase padre, lo que resuelve los problemas que encontramos en el modelo de herencia.

 

Otro tipo de problema de combinación es la abstracción de tipos decorativos. Para poder ilustrar esta situación, considere el siguiente ejemplo de herencia:

class FancyBox : View() { /* ... */ }
class Story : View() { /* ... */ }
class EditForm : FormView() { /* ... */ }
class FancyStory : ??? { /* ... */ }
class FancyEditForm : ??? { /* ... */ }

FancyBox es una vista que se usa para decorar otras vistas. En este ejemplo, se usará para decorar Story y EditForm. Queremos escribir FancyStory y FancyEditForm, pero ¿cómo hacerlo? ¿Deberíamos heredar de FancyBox o Story? Y debido a la limitación de una clase de padre único en la cadena de herencia, esto se vuelve muy vago.

 

Por el contrario, Compose puede manejar bien este problema:

@Composable
fun FancyBox(children: @Composable () -> Unit) {
  Box(fancy) { children() }
}
@Composable fun Story(…) { /* ... */ }
@Composable fun EditForm(...) { /* ... */ }
@Composable fun FancyStory(...) {
  FancyBox { Story(…) }
}
@Composable fun FancyEditForm(...) {
  FancyBox { EditForm(...) }
}

Usamos Composable lambda de niño, lo que nos permite definir algunas funciones que pueden envolver otras funciones. De esta forma, cuando queremos crear FancyStory, podemos llamar a Story en los hijos de FancyBox, y podemos usar FancyEditForm para hacer lo mismo. Este es el modelo de combinación de Compose.

Paquete

Otro aspecto que Compose hace bien es la "encapsulación". Esto es lo que debe tener en cuenta al crear una API de función componible pública: la API componible pública es solo un conjunto de parámetros que recibe, por lo que Compose no puede controlarlos. Por otro lado, la función Composable puede administrar y crear estados, y luego pasar el estado y cualquier dato que reciba a otras funciones Composable como parámetros.

 

Ahora, dado que está administrando el estado, si desea cambiar el estado, puede habilitar la función Composable de su hijo para notificar el cambio actual mediante una devolución de llamada.

Reorganización

"Reorganización" significa que cualquier función de Composable se puede volver a llamar en cualquier momento. Si tiene una enorme jerarquía de Composable, cuando una determinada parte de su jerarquía cambia, no desea volver a calcular toda la jerarquía. Entonces, la función Composable se puede reiniciar, puede usar esta función para lograr algunas funciones poderosas.

 

Por ejemplo, aquí hay una función Bind, que es un código común para el desarrollo de Android:

fun bind(liveMsgs: LiveData<MessageData>) {
  liveMsgs.observe(this) { msgs ->
    updateBody(msgs)
  }
}

Tenemos un LiveData y esperamos que la vista pueda suscribirse a él. Para hacer esto, llamamos al método de observación y pasamos un LifecycleOwner, y luego pasamos lambda. Se llamará a Lambda cada vez que se actualice LiveData, y cuando esto suceda, querremos actualizar la vista.

 

Usando Redactar, podemos revertir esta relación.

@Composable
fun Messages(liveMsgs: LiveData<MessageData>) {
 val msgs by liveMsgs.observeAsState()
 for (msg in msgs) {
   Message(msg)
 }
}

Hay una función de Composable similar: Mensajes. Recibe LiveData como parámetro y llama al método observeAsState de Compose. El método observeAsState asigna LiveData <T> a State <T>, lo que significa que puede usar su valor en el ámbito del cuerpo de la función. La instancia de State se suscribe a la instancia de LiveData, lo que significa que el estado se actualizará siempre que cambie LiveData. También significa que, independientemente de dónde se lea la instancia de State, la función Composable que la envuelve y se ha leído se suscribirá automáticamente a estas cambio. Como resultado, ya no es necesario especificar LifecycleOwner o actualizar devoluciones de llamada, Composable puede implementar implícitamente las funciones de ambos.

para resumir

Compose proporciona una forma moderna de definir su interfaz de usuario, lo que le permite lograr una separación de preocupaciones eficaz. Dado que las funciones de Composable son muy similares a las funciones ordinarias de Kotlin, las herramientas que usa para escribir y refactorizar la interfaz de usuario con Compose se conectarán perfectamente con su conocimiento del desarrollo de Android y las herramientas que usa.         

 

En el próximo artículo, me centraré en algunos detalles de implementación de Compose y su compilador. Para obtener más recursos sobre Redactar, haga clic en leer para leer el texto original y encontrar más información.


Lectura recomendada




 Haga clic en la pantalla al final leer leer el texto original  | Utilice Jetpack Compose más rápido para crear mejores aplicaciones  


Supongo que te gusta

Origin blog.csdn.net/jILRvRTrc/article/details/109212816
Recomendado
Clasificación