Universidad del Quindío
Programa de Ingeniería de Sistemas y Computación
Título: Formularios en Jetpack Compose
Docente: Carlos Andrés Florez V.
En la clase anterior, aprendimos a crear formularios básicos utilizando Jetpack Compose y a usar ViewModel para gestionar el estado y la lógica de validación. En esta clase, vamos a profundizar en la creación de formularios más avanzados, con otros composables como DropdownMenu, DatePicker y AlertDialog.
Un DropdownMenu es un componente que permite a los usuarios seleccionar una opción de una lista desplegable. Es útil cuando hay múltiples opciones y se desea ahorrar espacio en la interfaz de usuario. Jetpack Compose proporciona un composable llamado ExposedDropdownMenuBox que facilita la creación de menús desplegables.
Por ejemplo, para crear un menú desplegable de selección de ciudad en un formulario de registro, podemos usar el siguiente código:
package com.example.demoapp.core.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@OptIn(ExperimentalMaterial3Api::class) // Algunos composables aún están en fase experimental
@Composable
fun DropdownMenu(
value: String,
label: String,
supportingText: String? = null,
list: List<String>,
icon: ImageVector? = null,
onValueChange: (String) -> Unit,
enabled: Boolean = true
) {
// Estado interno para controlar si el menú está expandido o no
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded && enabled,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
enabled = enabled,
readOnly = true,
value = value, // Muestra el valor seleccionado
onValueChange = { },
label = {
Text(text = label) // Muestra la etiqueta del campo
},
supportingText = supportingText?.let {
{ Text(text = supportingText) } // Muestra el texto de soporte si se proporciona
},
leadingIcon = icon?.let {
{ Icon(imageVector = icon, contentDescription = null) } // Muestra el icono si se proporciona
},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded && enabled) }, // Icono de flecha para el menú desplegable
modifier = Modifier
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded && enabled,
onDismissRequest = { expanded = false }
) {
// Genera un elemento del menú para cada opción en la lista
list.forEach {
DropdownMenuItem(
text = {
Text(text = it)
},
onClick = {
onValueChange(it) // Actualiza el valor seleccionado al hacer clic
expanded = false // Cierra el menú desplegable
}
)
}
}
}
}
La idea es utilizar este DropdownMenu dentro de un formulario, gestionando su estado desde un ViewModel. Por lo tanto, cree este composable en el paquete com.example.demoapp.core.components de su proyecto.
Para hacer uso de este DropdownMenu, puede integrarlo en su formulario de registro de la siguiente manera:
@Composable
fun RegisterScreen(viewModel: RegisterViewModel = viewModel()) {
// Resto del formulario...
DropdownMenu(
value = viewModel.city.value,
onValueChange = { viewModel.city.onChange(it) },
label = "Ciudad",
icon = Icons.Default.Home,
list = viewModel.cities,
)
// Resto del formulario...
}
Además, se debe ajustar el ViewModel para manejar el estado del campo de ciudad:
class RegisterViewModel : ViewModel() {
// Resto del ViewModel...
val cities = listOf("Ciudad 1", "Ciudad 2", "Ciudad 3")
val city = ValidatedField("") { value ->
if (value.isEmpty()) "Selecciona una ciudad" else null
}
// Resto del ViewModel...
}
Haga las modificaciones necesarias en su proyecto para integrar este DropdownMenu en el formulario de registro. Luego, ejecute la aplicación y verifique que el menú desplegable funcione correctamente.
AlertDialog)Un AlertDialog es un cuadro de diálogo modal que presenta información importante o solicita una acción del usuario. Se utiliza comúnmente para confirmar acciones o mostrar mensajes críticos. Por ejemplo, en el formulario de registro, podemos usar un AlertDialog para controlar el envío del formulario.
Agregue la siguiente función composable para mostrar un AlertDialog de confirmación:
@Composable
fun ConfirmAlertDialog(
onShowExitDialogChange: (Boolean) -> Unit,
onConfirm: () -> Unit
) {
AlertDialog(
title = { Text(text = "¿Está seguro de enviar los datos?") },
text = { Text(text = "Está a punto de enviar los datos al servidor.") },
onDismissRequest = { onShowExitDialogChange(false) },
confirmButton = {
TextButton(
onClick = {
onShowExitDialogChange(false)
onConfirm()
}
) {
Text("Confirmar")
}
},
dismissButton = {
TextButton(
onClick = { onShowExitDialogChange(false) }
) {
Text("Cerrar")
}
}
)
}
Ahora, modifique el formulario para mostrar este diálogo cuando el usuario intente enviar los datos. Primero agregue un estado para controlar la visibilidad del diálogo dentro de la función composable del formulario:
var showConfirmDialog by remember { mutableStateOf(false) }
Ahora, en el botón de envío, actualice el estado para mostrar el diálogo:
Button(
onClick = {
showConfirmDialog = true
},
enabled = viewModel.isFormValid,
content = {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Icono de persona"
)
Spacer(modifier = Modifier.width(width = 5.dp))
Text(text = "Register")
}
)
Finalmente, agregue el ConfirmAlertDialog al final del formulario para que se muestre cuando showExitDialog sea verdadero:
if (showConfirmDialog) {
ConfirmAlertDialog(
onShowExitDialogChange = { showConfirmDialog = it },
onConfirm = {
// Lógica para enviar los datos del formulario, por ahora solo un log
Log.d("Register", "Email: ${viewModel.email.value}, Password: ${viewModel.password.value}")
}
)
}
Ejecute la aplicación y pruebe el formulario. Al hacer clic en el botón de envío, debería aparecer el cuadro de diálogo de confirmación.
Para más información sobre cómo implementar un AlertDialog, puede consultar la documentación oficial de material y la guía de Jetpack Compose.
Un DatePicker es un componente que permite a los usuarios seleccionar una fecha de un calendario. Es útil para formularios que requieren la entrada de fechas, como fechas de nacimiento o citas. Jetpack compose tiene dos composables populares para seleccionar fechas: DatePicker y DatePickerDialog.
Para más información sobre cómo integrar un DatePicker, puede consultar la documentación oficial de material y la guía de Jetpack Compose.
Aunque Jetpack Compose incluye algunos iconos básicos en la biblioteca androidx.compose.material:material-icons-core en algunos casos puede que necesitemos una gama más amplia de iconos para mejorar la experiencia del usuario en nuestros formularios.
Google ofrece una biblioteca llamada material-icons-extended que proporciona una amplia variedad de iconos adicionales basados en las directrices de Material Design. A continuación, se explica cómo agregar y utilizar estos iconos en sus formularios, botones, y otros componentes de la interfaz de usuario.
Agregue lo siguiente en el archivo libs.versions.toml:
[versions]
material-icons-extended = "1.7.8"
[libraries]
material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" }
Luego, agregue la dependencia en el archivo build.gradle.kts de su módulo:
dependencies {
implementation(libs.material.icons.extended)
}
Sincronice su proyecto para descargar la dependencia.
Para usar un icono en un campo de entrada, importe el icono deseado y utilícelo en el parámetro leadingIcon o trailingIcon del OutlinedTextField. Por ejemplo, para agregar un icono de correo electrónico al campo de correo electrónico en el formulario de registro:
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
OutlinedTextField(
value = viewModel.email.value,
onValueChange = { viewModel.email.onChange(it) },
label = { Text("Email") },
leadingIcon = {
Icon(
imageVector = Icons.Default.Email,
contentDescription = "Icono de correo electrónico"
)
},
// Otros parámetros...
)
O en los botones:
Button(
onClick = {
viewModel.login()
},
enabled = viewModel.isFormValid,
content = {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Icono de login"
)
Spacer(modifier = Modifier.width(width = 5.dp))
Text(text = "Iniciar Sesión")
}
)
Para estos ejemplos se tomó como referencia el formulario de login visto en la clase anterior. Asegúrese de importar los iconos que desea utilizar desde androidx.compose.material.icons.filled o androidx.compose.material.icons.outlined, dependiendo del estilo que prefiera.
Puede explorar la lista completa de iconos disponibles en la documentación oficial de Material Icons. Allí encontrará iconos para diversas categorías y propósitos, que puede utilizar para mejorar la experiencia del usuario en sus formularios. Tenga en cuenta que los iconos vienen en diferentes estilos, como Filled, Outlined, Rounded, TwoTone y Sharp. Asegúrese de elegir el estilo que mejor se adapte al diseño de su aplicación.
Supongamos que, al momento de registrar un usuario, queremos permitirle seleccionar una imagen de perfil. Jetpack Compose facilita la carga y visualización de imágenes utilizando la biblioteca Coil.
Aquí hay un ejemplo de cómo cargar y mostrar una imagen desde una URL utilizando Coil en Jetpack Compose:
Para agregar la dependencia de Coil en su proyecto, primero agrege lo siguiente en su archivo libs.versions.toml:
[versions]
coil = "3.3.0"
[libraries]
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" }
Luego, agregue la dependencia en el archivo build.gradle.kts de su módulo:
dependencies {
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
}
Sincronice su proyecto para descargar la dependencia.
Si va a cargar imágenes desde una URL, asegúrese de agregar el permiso de Internet en su archivo AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Esto es necesario para que la aplicación pueda acceder a recursos en línea, de no hacerlo, la carga de imágenes desde URLs fallará.
Para cargar y mostrar una imagen desde una URL, puede utilizar el composable AsyncImage proporcionado por Coil. Aquí hay un ejemplo de cómo hacerlo:
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@Composable
fun ProfileImage(url: String) {
AsyncImage(
contentScale = ContentScale.Crop,
model = url,
contentDescription = "Imagen de perfil",
modifier = Modifier.size(100.dp) // Ajusta el tamaño según sea necesario
)
}
AsyncImage maneja automáticamente la carga de la imagen, el almacenamiento en caché y la visualización. Sus principales parámetros son:
model: Aquí se especifica la URL de la imagen que se desea cargar.contentScale: Define cómo se escala la imagen dentro del contenedor. Las opciones comunes incluyen ContentScale.Crop, ContentScale.Fit, etc.contentDescription: Una descripción de la imagen para accesibilidad.modifier: Permite ajustar el tamaño y otros aspectos visuales de la imagen.Puede personalizar aún más la carga de imágenes con Coil, como agregar un placeholder mientras se carga la imagen o manejar errores si la imagen no se puede cargar. Aquí hay un ejemplo de cómo hacerlo:
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url) // URL de la imagen
.crossfade(true) // Efecto de desvanecimiento al cargar
.placeholder(R.drawable.placeholder) // Imagen de marcador de posición
.error(R.drawable.error) // Imagen de error
.build(),
contentDescription = "Imagen de perfil",
modifier = Modifier.size(100.dp)
)
Incluso, si se desea configurar la forma de la imagen (por ejemplo, hacerla circular), se puede utilizar el modificador clip junto con RoundedCornerShape:
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.width(95.dp)
.height(95.dp)
Para más información sobre cómo cargar imágenes con Coil, puede consultar la documentación oficial.
Agregue un campo de selección de fecha al formulario de registro utilizando un DatePickerDialog. Gestione su estado desde el ViewModel y asegúrese de que la fecha seleccionada se valide correctamente (por ejemplo, no permitir fechas futuras para una fecha de nacimiento).
Investigue cómo agregar y eliminar campos de entrada de forma dinámica en Jetpack Compose. Por ejemplo, permita a los usuarios agregar múltiples números de teléfono en el formulario de registro.
Ajuste el formulario de registro para permitir a los usuarios pegar la URL de una imagen de perfil. Utilice Coil para mostrar la imagen en el formulario antes de enviarlo. Más adelante veremos cómo permitir la carga de imágenes desde la galería del dispositivo.
Explore la biblioteca de iconos extendidos de Material Design y aplique iconos relevantes a los campos del formulario de registro, como un icono de calendario para el campo de fecha de nacimiento o un icono de teléfono para el campo de número de teléfono, etc.