Librería de componentes reutilizables de Jetpack Compose con Material 3 de Google, construida con Kotlin Multiplatform y Compose Multiplatform: escribe la UI una vez y úsala en:
| Plataforma | Target |
|---|---|
| Android | androidTarget |
| Escritorio (Windows/macOS/Linux) | jvm("desktop") |
| iOS | iosArm64, iosSimulatorArm64, iosX64 |
| Web | wasmJs |
Licencia MIT — úsala libremente en cualquier proyecto.
Disponible en Maven Central — no necesitas añadir ningún repositorio extra:
// build.gradle.kts (módulo)
dependencies {
implementation("io.github.ricardomorarey:compose-ui-material3:0.1.0")
}En un proyecto Kotlin Multiplatform, añádela a commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.ricardomorarey:compose-ui-material3:0.1.0")
}
}
}Para probar cambios locales sin publicar, puedes instalarla en tu Maven local con ./gradlew publishToMavenLocal y consumirla añadiendo mavenLocal() a tus repositorios.
Todos usan MaterialTheme de Material 3, así que heredan automáticamente los colores, tipografía y formas del tema de tu app.
// Botón con estado de carga integrado
LoadingButton(
text = "Guardar",
loading = isSaving,
onClick = { viewModel.save() },
)
SecondaryButton(text = "Cancelar", onClick = { /* ... */ })
// Botón con degradado (por defecto primary -> tertiary del tema)
GradientButton(text = "Empezar", onClick = { /* ... */ })
// Acciones destructivas (usa los colores de error del tema)
DestructiveButton(text = "Eliminar", onClick = { /* ... */ })InfoCard(
title = "Ajustes",
subtitle = "Notificaciones, tema, idioma",
onClick = { /* navegar */ },
)
ExpandableCard(title = "Detalles del pedido") {
Text("Contenido que se despliega con animación")
}LabeledTextField(
value = email,
onValueChange = { email = it },
label = "Correo",
errorMessage = if (emailValido) null else "Correo no válido",
)
PasswordTextField(value = password, onValueChange = { password = it })
SearchField(value = query, onValueChange = { query = it })// Selección múltiple
FilterChipGroup(
options = listOf("Kotlin", "Compose", "KMP"),
selectedOptions = selected,
onSelectionChange = { selected = it },
)
// Selección única
ChoiceChipRow(
options = listOf("Día", "Semana", "Mes"),
selectedOption = period,
onOptionSelected = { period = it },
)if (showDeleteDialog) {
ConfirmDialog(
title = "Eliminar elemento",
message = "Esta acción no se puede deshacer.",
destructive = true,
onConfirm = { viewModel.delete() },
onDismiss = { showDeleteDialog = false },
)
}InlineBanner(
message = "Hay una nueva versión disponible.",
severity = BannerSeverity.Info, // Info, Success, Warning o Error
actionText = "Actualizar",
onAction = { /* ... */ },
onDismiss = { /* muestra una X para descartar */ },
)SwitchRow(
title = "Notificaciones",
subtitle = "Avisos de actividad en tu cuenta",
checked = notificationsEnabled,
onCheckedChange = { notificationsEnabled = it },
)
CheckboxRow(
title = "Boletín semanal",
checked = newsletter,
onCheckedChange = { newsletter = it },
)
RadioGroup(
options = listOf("Claro", "Oscuro", "Sistema"),
selectedOption = theme,
onOptionSelected = { theme = it },
)// Iniciales con color determinista derivado del nombre
InitialsAvatar(name = "Ada Lovelace")
// Grupo solapado; si hay más de maxVisible muestra "+N"
AvatarGroup(
names = listOf("Ada Lovelace", "Grace Hopper", "Alan Turing", /* ... */),
maxVisible = 4,
)// Selector de cantidad (carritos, entradas...)
QuantityStepper(value = qty, onValueChange = { qty = it }, range = 0..99)
// Progreso de un asistente por pasos
StepProgressIndicator(
steps = listOf("Carrito", "Envío", "Pago", "Confirmar"),
currentStep = 2,
)FullScreenLoading(message = "Cargando…")
LoadingOverlay(visible = isLoading) {
MiContenido()
}
// Placeholder shimmer
Box(Modifier.fillMaxWidth().height(20.dp).shimmer())EmptyState(
title = "Sin resultados",
message = "Prueba con otros filtros",
actionText = "Limpiar filtros",
onAction = { /* ... */ },
)
ErrorState(
title = "Algo salió mal",
message = error.message,
onRetry = { viewModel.reload() },
)RatingBar(
rating = rating,
onRatingChange = { rating = it }, // null para solo lectura
)SectionHeader(title = "Populares", actionText = "Ver todo", onAction = { /* ... */ })
CounterBadge(count = 128) // muestra "99+"
// Divisor con etiqueta centrada
LabeledDivider(text = "o continúa con")El módulo demo/ es una galería interactiva de escritorio con todos los componentes. Para lanzarla:
./gradlew :demo:runLa referencia completa (generada con Dokka a partir de los KDoc) se publica automáticamente en
https://ricardomorarey.github.io/librer-a-compose-ui-material3-/ con cada push a main.
Para generarla en local: ./gradlew :library:dokkaGenerate (queda en library/build/dokka/html).
- La librería usa el modo
explicitApi()de Kotlin: toda la API pública declara su visibilidad de forma explícita. - El binary compatibility validator vigila que ninguna release rompa la API sin querer: los ficheros
library/api/*.apidescriben la API pública y el CI falla si cambian sin actualizarse. Tras un cambio intencionado de API, ejecuta./gradlew :library:apiDumpy revisa el diff en el commit.
./gradlew :library:assemble- En Windows/Linux se compilan Android, Desktop y Web (los targets de iOS se omiten automáticamente).
- En macOS se compilan todos los targets, incluido iOS.
Las capturas del README se generan renderizando los componentes con Compose Desktop (sin emulador):
./gradlew :library:generateScreenshotsTodo el ciclo está automatizado en el workflow release.yml:
- Ve a Actions → Release → Run workflow e indica la versión (por ejemplo
0.2.0). - El workflow actualiza
VERSION_NAMEengradle.properties(con commit incluido), publica en Maven Central (compilando en macOS para incluir iOS) y crea la release en GitHub con el tagvX.Y.Zy notas generadas.
También puede hacerse a la antigua: crear una release a mano en GitHub dispara publish.yml, que solo publica (en ese caso, recuerda subir antes VERSION_NAME).
La publicación usa el Central Portal de Sonatype mediante el plugin de Vanniktech. Ninguna credencial vive en este repositorio: los workflows leen los secrets MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, SIGNING_KEY y SIGNING_KEY_PASSWORD de Settings → Secrets and variables → Actions.
local.properties(rutas locales de tu máquina) está en.gitignorey no se sube.- Las claves de firma y tokens solo se leen de variables de entorno / GitHub Secrets.
- El
.gitignorebloquea además*.jks,*.keystore,*.gpg,*.ascy similares por si acaso.
MIT © ricardomorarey










