Construcción de aplicaciones móviles

Universidad del Quindío
Programa de Ingeniería de Sistemas y Computación
Título: Composables en Jetpack Compose
Docente: Carlos Andrés Florez V.

Composables en Jetpack Compose

Introducción

Un “composable” es una función en Jetpack Compose que define una parte de la interfaz de usuario (UI) de una aplicación Android. Estas funciones son anotadas con @Composable y permiten construir interfaces de usuario de manera declarativa, lo que significa que se describe cómo debería verse la UI en función del estado actual de los datos.

La idea es construir la UI utilizando pequeñas piezas reutilizables (composables) que pueden combinarse para formar interfaces más complejas. Esto facilita la creación, el mantenimiento y la actualización de la UI, ya que cada composable puede ser desarrollado y probado de manera independiente.

Dado que un composable es simplemente una función, puede aceptar parámetros y utilizar otras funciones composables dentro de su definición. Esto permite una gran flexibilidad y modularidad en el diseño de la UI.

Composables básicos

Jetpack Compose proporciona varios composables básicos que podemos utilizar para construir nuestra UI. Algunos de los más comunes incluyen:

Estos composables pueden ser combinados y anidados para crear interfaces de usuario complejas y dinámicas. Además, cada composable recibe parámetros que permiten personalizar su apariencia y comportamiento.

Text

El composable Text se utiliza para mostrar texto en la pantalla. Aquí hay un ejemplo simple de cómo usarlo:

Text(text = "Hello, Jetpack Compose!")

Este composable también puede aceptar parámetros adicionales para personalizar la apariencia del texto, como el tamaño de fuente, el color y el estilo. Por ejemplo:

Text(
    text = "Hello, Jetpack Compose!",
    fontSize = 24.sp,
    color = Color.Blue,
    fontWeight = FontWeight.Bold
)

⚠️ Importante: El tamaño de fuente se especifica en sp (scale-independent pixels), que es una unidad recomendada para el texto en Android.

El color y el peso de la fuente también se pueden personalizar utilizando las clases proporcionadas por Jetpack Compose, aunque es necesario importar los paquetes correspondientes para que el código funcione correctamente.

Estilos tipográficos de Material Design

En lugar de definir tamaños y pesos manualmente, se recomienda usar el parámetro style con la escala tipográfica que proporciona Material Design 3. Esto garantiza consistencia visual con el sistema de diseño de la aplicación:

Text(
    text = "Título principal", 
    style = MaterialTheme.typography.headlineLarge
)
Text(
    text = "Subtítulo",        
    style = MaterialTheme.typography.titleMedium
)
Text(
    text = "Cuerpo de texto",  
    style = MaterialTheme.typography.bodyLarge
)
Text(
    text = "Etiqueta pequeña", 
    style = MaterialTheme.typography.labelSmall
)

Material Design 3 organiza la tipografía en cinco grupos, cada uno con tres tamaños (Large, Medium, Small):

Grupo Uso típico
display Textos muy grandes y decorativos
headline Títulos de pantalla o sección
title Encabezados de tarjetas o listas
body Texto de contenido principal
label Etiquetas, botones y textos pequeños

⚠️ Importante: Al usar MaterialTheme.typography, los estilos se adaptan automáticamente al tema de la aplicación (claro u oscuro), lo que facilita mantener una apariencia coherente en toda la UI.

Button

El composable Button se utiliza para crear un botón interactivo. Aquí hay un ejemplo de cómo usarlo:

Button(
    onClick = { /* Acción al hacer clic */ },
    content = {
        Text(text = "Hacer algo")
    }
)

El botón puede contener otros composables, como Text, para definir su contenido. El parámetro onClick define la acción que se ejecutará cuando el usuario haga clic en el botón.

Existen variantes del botón, como OutlinedButton y TextButton, que ofrecen diferentes estilos visuales.

Personalización de botones

El parámetro shape permite cambiar la forma de los bordes del botón. Material Design 3 ofrece formas predefinidas que se pueden usar directamente:

Button(
    onClick = { },
    shape = RoundedCornerShape(8.dp) // Esquinas ligeramente redondeadas
) {
    Text("Hacer algo")
}

Algunas formas comunes son:

Shape Descripción
RoundedCornerShape(50%) Bordes completamente circulares (por defecto en M3)
RoundedCornerShape(8.dp) Esquinas ligeramente redondeadas
RectangleShape Sin redondeo, esquinas rectas
CutCornerShape(8.dp) Esquinas cortadas en diagonal

El color de fondo del botón se controla con el parámetro colors, usando ButtonDefaults.buttonColors():

Button(
    onClick = { },
    colors = ButtonDefaults.buttonColors(
        containerColor = Color.DarkGray,  // Color de fondo
        contentColor = Color.White        // Color del contenido (texto e iconos)
    )
) {
    Text("Hacer algo")
}

⚠️ Importante: En Material Design 3 el fondo del botón se llama containerColor, no backgroundColor. Si no se especifica, toma automáticamente el color primary del tema de la aplicación.

Para más información sobre botones y sus variantes, se puede consultar la documentación oficial de Jetpack Compose.

Layouts básicos: Column, Row y Box

Los composables Column, Row y Box son fundamentales para organizar otros elementos en la pantalla. Cada uno tiene un propósito específico para la disposición de los elementos hijos:

Box

El composable Box se utiliza para apilar elementos unos sobre otros. Es útil cuando se desea superponer elementos o crear diseños más complejos. Aquí hay un ejemplo de cómo usarlo:

Box {
    Text(text = "Texto de fondo")
    Text(
        text = "Texto superpuesto",
        modifier = Modifier.align(Alignment.Center) // Centra el texto superpuesto
    )
}

Column y Row

Los composables Column y Row se utilizan para organizar otros composables en una disposición vertical u horizontal, respectivamente. Aquí hay ejemplos de cómo usarlos:

En este caso, Column organiza los elementos en una columna vertical, es decir, uno debajo del otro:

Column {
    Text(text = "Elemento 1")
    Text(text = "Elemento 2")
    Text(text = "Elemento 3")
}

Row organiza los elementos en una fila horizontal, es decir, uno al lado del otro:

Row {
    Text(text = "Elemento A")
    Text(text = "Elemento B")
    Text(text = "Elemento C")
}

Adicionalmente, ambos composables pueden aceptar parámetros para personalizar la alineación, el espaciado y otros aspectos de la disposición de sus elementos hijos. Por ejemplo, podemos agregar espaciado entre los elementos en una Column:

Column(
    verticalArrangement = Arrangement.spacedBy(8.dp)
) {
    Text(text = "Elemento 1")
    Text(text = "Elemento 2")
    Text(text = "Elemento 3")
}

⚠️ Importante: El espaciado se especifica en dp (density-independent pixels), que es una unidad recomendada para el diseño de interfaces en Android, no se recomienda usar píxeles absolutos ya que pueden variar entre diferentes dispositivos.

O podemos alinear los elementos en una Row al centro tanto horizontal como verticalmente:

Row(
    horizontalArrangement = Arrangement.Center,
    verticalAlignment = Alignment.CenterVertically
) {
    Text(text = "Elemento A")
    Text(text = "Elemento B")
    Text(text = "Elemento C")
}

Tanto Row como Column son fundamentales para construir layouts en Jetpack Compose, ya que permiten organizar y estructurar la UI de manera flexible y sencilla.

Existe una variación llamada LazyColumn y LazyRow, que son versiones optimizadas de Column y Row para listas largas de elementos. Estas versiones solo renderizan los elementos que están visibles en la pantalla, lo que mejora el rendimiento al manejar grandes conjuntos de datos.

Comportamiento de Box, Column y Row

La siguiente imagen ilustra el comportamiento que tienen los composables Box, Column y Row al organizar sus elementos hijos:

Column y Row *Fuente: Documentación oficial de Jetpack Compose

Image

El composable Image se utiliza para mostrar imágenes en la pantalla. Aquí hay un ejemplo de cómo usarlo:

Image(
    painter = painterResource(id = R.drawable.mi_imagen),
    contentDescription = "Descripción de la imagen"
)

Observe que el parámetro painter se utiliza para cargar la imagen desde los recursos de la aplicación, y contentDescription proporciona una descripción accesible de la imagen para usuarios con discapacidades visuales. En este caso, R.drawable.mi_imagen es una referencia a una imagen almacenada en la carpeta res/drawable de su proyecto.

Icon

El composable Icon se utiliza para mostrar iconos vectoriales en la pantalla. Aquí hay un ejemplo de cómo usarlo:

Icon(
    imageVector = Icons.Default.Favorite,
    contentDescription = "Icono de favorito"
)

Android proporciona una colección de iconos prediseñados en la biblioteca Icons.Default, pero también es posible utilizar iconos personalizados cargándolos desde recursos vectoriales.

TextField

El composable TextField se utiliza para permitir la entrada de texto por parte del usuario. Aquí hay un ejemplo de cómo usarlo:

TextField(
    value = text,
    onValueChange = { /* Actualizar el valor del texto */ },
    label = { 
        Text("Ingrese su nombre") 
    }
)

En este ejemplo, value representa el texto actual en el campo de texto, y onValueChange es una función que se llama cada vez que el usuario modifica el texto, ya que debe recomponer el composable con el nuevo valor. El parámetro label proporciona una etiqueta descriptiva para el campo de texto.

Existen algunas variantes de TextField, como OutlinedTextField, que muestra un borde alrededor del campo de texto.

Para más información sobre campos de texto y sus variantes, se puede consultar la documentación oficial de Jetpack Compose.


Creación de un Composable

Así como se mencionó anteriormente, un composable es una función anotada con @Composable que puede recibir parámetros y definir la UI utilizando otros composables. Un composable puede ser tan simple o complejo como sea necesario, y puede reutilizarse en diferentes partes de la aplicación si se define de manera adecuada.

Dado que un composable puede contener otros composables, es posible crear una jerarquía de composables para construir interfaces de usuario más complejas. Por ejemplo, un composable de pantalla principal podría contener varios composables hijos que representan diferentes secciones de la pantalla, como un encabezado, un cuerpo y un pie de página.

La siguiente imagen ilustra cómo un composable puede contener otros composables, formando una jerarquía de UI:

Composable básico *Fuente: Documentación oficial de Jetpack Compose

Ejemplo pantalla inicial (Home Screen)

Vamos a crear un composable simple que represente una pantalla inicial con una imagen, un texto de bienvenida y un botón para comenzar. Aquí está el código:

@Composable
fun HomeScreen(){
    // Estructura de la pantalla inicial, sus hijos se organizan uno debajo del otro
    Column{
        // Carga una imagen desde los recursos de la aplicación (res/drawable)
        Image(
            painter = painterResource(R.drawable.welcome),
            contentDescription = "Welcome Image"
        )
        // Muestra un texto de bienvenida
        Text(text = "Pantalla de bienvenida")
        // Organiza los botones en una fila horizontal
        Row{
            Button(
                onClick = {
                    // Acción al hacer clic en el botón de inicio de sesión
                }
            ) {
                Text(text = "Iniciar sesión")
            }
            Button(
                onClick = {
                    // Acción al hacer clic en el botón de registro
                }
            ) {
                Text(text = "Crear una cuenta")
            }
        }
    }

}

Debe importar los siguientes paquetes para que el código funcione correctamente:

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import com.example.demoapp.R

Abra el proyecto de Android Studio que creó en la guía anterior. En el archivo MainActivity.kt, agregue esta nueva función y modifique la función setContent para utilizar el composable HomeScreen que definimos anteriormente, borre el contenido existente y reemplácelo con el siguiente código:

setContent {
    DemoAppTheme {
        HomeScreen()
    }
}

⚠️ Importante: asegúrese de tener una imagen llamada welcome.png o welcome.jpg en la carpeta res/drawable de su proyecto para que el composable Image funcione correctamente.

Ejecute la aplicación en un emulador o dispositivo físico. Debería ver una pantalla inicial simple con una imagen, un texto de bienvenida y dos botones para iniciar sesión y registrarse.

Para mejorar un poco la apariencia de la pantalla inicial, vamos a modificar el Column para centrar los elementos en la pantalla y agregar algo de espacio entre ellos. Actualice el composable HomeScreen de la siguiente manera:

@Composable
fun HomeScreen(){
    Column(
        modifier = Modifier.fillMaxSize(), // Ocupa todo el espacio disponible
        verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterVertically), // Espacio entre elementos y centrado vertical
        horizontalAlignment = Alignment.CenterHorizontally // Centrado horizontal
    ) {
        Image(
            painter = painterResource(R.drawable.welcome),
            contentDescription = "Welcome Image"
        )
        Text(text = "Home Screen")
        Row(
            horizontalArrangement = Arrangement.spacedBy(15.dp, Alignment.CenterHorizontally), // Espacio entre botones y centrado horizontal
            verticalAlignment = Alignment.CenterVertically // Centrado vertical
        ) {
            Button(
                onClick = {
                    // Acción al hacer clic en el botón de inicio de sesión
                }
            ) {
                Text(text = "Iniciar sesión")
            }
            Button(
                onClick = {
                    // Acción al hacer clic en el botón de registro
                }
            ) {
                Text(text = "Crear una cuenta")
            }
        }

    }
}

Debe importar los siguientes paquetes para que el código funcione correctamente:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

Ejecute nuevamente la aplicación. Ahora debería ver la pantalla inicial centrada en la pantalla con un espacio adecuado entre los elementos. Obviamente, este es un ejemplo muy simple y básico, pero ilustra cómo crear y utilizar composables personalizados en Jetpack Compose para construir interfaces de usuario.

Más adelante, implementaremos la funcionalidad real para los botones de inicio de sesión y registro.

Ejemplo formulario de inicio de sesión

Ahora, vamos a crear un composable que represente un formulario de inicio de sesión con campos para el nombre de usuario y la contraseña, así como un botón para iniciar sesión. Agregue la siguiente función en el archivo MainActivity.kt:

@Composable
fun LoginScreen() {
    Column {
        Text(
            text = "Email"
        )
        TextField(
            value = "", 
            onValueChange = {}
        )
        Text(
            text = "Password"
        )
        TextField(
            value = "", 
            onValueChange = {}, 
            visualTransformation = PasswordVisualTransformation()
        )
        Button(
            onClick = { /* Acción de inicio de sesión */ },
            content = {
                Text(text = "Iniciar sesión")
            }
        )
    }
}

Debe importar los siguientes paquetes para que el código funcione correctamente:

import androidx.compose.material3.TextField
import androidx.compose.ui.text.input.PasswordVisualTransformation

Ahora modifique la función setContent del MainActivity.kt para utilizar el composable LoginScreen que definimos anteriormente, borre el contenido existente y reemplácelo con el siguiente código:

setContent {
    DemoAppTheme {
        LoginScreen()
    }
}

Ejecute la aplicación en un emulador o dispositivo físico. Debería ver un formulario de inicio de sesión simple con campos para el nombre de usuario y la contraseña, así como un botón de inicio de sesión.

Mejore un poco la apariencia del formulario, actualice el composable LoginScreen para que los elementos estén centrados en la pantalla y agregue algo de espacio entre ellos., puede basarse en el ejemplo anterior del HomeScreen para lograrlo.

⚠️ Importante: Aunque este formulario no tiene funcionalidad real (los campos no almacenan datos y el botón no realiza ninguna acción), este ejemplo ilustra cómo crear y utilizar composables personalizados en Jetpack Compose para construir interfaces de usuario.


Arquitectura del proyecto

A medida que una aplicación crece en complejidad, es importante organizar el código de manera efectiva para facilitar su mantenimiento y escalabilidad. Una buena práctica es separar los composables en diferentes archivos y paquetes según su funcionalidad. Esto ayuda a mantener el código limpio y fácil de navegar.

Estructura recomendada de paquetes

Por esto, es recomendable crear una estructura de paquetes adecuada para organizar los archivos de composables. Una posible estructura podría ser la siguiente:

com.example.demoapp/            # Raíz del proyecto
├── core/                       # Código común para toda la aplicación
│   ├── component/              # Composables reutilizables en toda la app
│   ├── navigation/             # Navegación principal de la app
│   ├── theme/                  # Temas y estilos de la app (Estos archivos se generan automáticamente al crear un proyecto con Jetpack Compose)
│   │   ├── Color.kt
│   │   ├── Theme.kt
│   │   └── Type.kt
│   └── utils/                  # Utilidades y funciones comunes
├── data/                       # Capa de datos 
│   ├── model/                  # Modelos de datos comunes (DTOs)
│   └── repository/             # Implementaciones de repositorios de datos
├── domain/                     # Lógica de negocio 
│   ├── model/                  # Modelos de dominio (entidades)
│   └── repository/             # Interfaces de repositorios de dominio
├── features/                   # Funcionalidades específicas de la app
│   ├── home/
│   ├── login/
│   └── register/
└── MainActivity.kt             # Actividad principal de la app

Este es solo un ejemplo de cómo podría organizarse un proyecto de Jetpack Compose. La estructura exacta puede variar según las necesidades específicas de la aplicación. Por ahora, cree los paquetes siguiendo esta estructura básica y mueva los archivos de theme.ui al paquete core/theme, no importa si algunos paquetes quedan vacíos por ahora.

Separación de composables en archivos

Cree un archivo llamado LoginScreen.kt en el paquete features/login y mueva el composable LoginScreen a este archivo.

Ahora, en el archivo MainActivity.kt, importe el composable LoginScreen desde su nuevo archivo:

import com.example.demoapp.features.login.LoginScreen

Ejecute la aplicación nuevamente para asegurarse de que todo funcione correctamente después de la separación. Debería ver el mismo formulario de inicio de sesión que antes, pero ahora el código está mejor organizado y es más fácil de mantener. Además el MainActivity.kt es más limpio y enfocado en la configuración de la actividad principal.

Haga lo mismo para el HomeScreen, creando un archivo llamado HomeScreen.kt en el paquete features/home y moviendo el composable HomeScreen a este archivo. Luego, importe el composable HomeScreen en el MainActivity.kt y asegúrese de que todo funcione correctamente.


Modificadores

Todos los composables en Jetpack Compose pueden ser personalizados utilizando modificadores. Los modificadores son objetos que permiten cambiar la apariencia, el comportamiento y la disposición de los composables. Se aplican utilizando el parámetro modifier que está disponible en la mayoría de los composables.

Por ejemplo, podemos modificar el composable Text para agregarle un relleno alrededor del texto de la siguiente manera:

Text(
    text = "Hello, Android Developer!",
    modifier = Modifier.padding(16.dp)
)

O podemos cambiar el tamaño o padings de un botón:

Button(
    onClick = { /* Acción al hacer clic */ },
    modifier = Modifier
        .size(width = 200.dp, height = 60.dp)
        .padding(8.dp)
    content = {
        Text(text = "Hacer algo")
    }
)

Hay muchos modificadores disponibles en Jetpack Compose, como background, border, clickable, fillMaxWidth, fillMaxHeight, fillMaxSize, entre otros. Estos modificadores se pueden encadenar para aplicar múltiples cambios a un composable.

⚠️ Importante: Tenga en cuenta que el orden de los modificadores puede afectar el resultado final. Es decir, si por ejemplo primero aplica un padding y luego un background, el fondo se aplicará al área sin el padding, mientras que si aplica primero el background y luego el padding, el fondo cubrirá todo el área incluyendo el padding.

Para más información sobre los modificadores disponibles y cómo usarlos, se puede consultar la documentación oficial de Jetpack Compose.


Actividad práctica

1. Preview de Composables

Jetpack Compose proporciona una función de vista previa que permite ver cómo se verá un composable sin necesidad de ejecutar la aplicación en un emulador o dispositivo físico. Esto es especialmente útil durante el desarrollo, ya que permite iterar rápidamente sobre el diseño de la UI. Para habilitar la vista previa de un composable, simplemente agregue la anotación @Preview encima de la función composable que desea previsualizar.

Use @Preview en el composable LoginScreen y HomeScreen para ver cómo se ven en la ventana de vista previa de Android Studio.

2. Modifiers

Investigue y utilice al menos tres modificadores diferentes en el composable LoginScreen para mejorar su apariencia y disposición. Por ejemplo, puede agregar un fondo, cambiar el tamaño de los campos de texto o agregar bordes a los botones.

3. Surface y Scaffold

Investigue acerca de los composables Surface y Scaffold en Jetpack Compose. Para qué sirven, cómo se utilizan y cuáles son sus beneficios.

4. Revisar documentación oficial

Se recomienda revisar la documentación oficial de Jetpack Compose para conocer y tener una idea general de los diferentes composables disponibles: